From 7c6ad61f50b8885ea3bc44443b2041126d80e380 Mon Sep 17 00:00:00 2001 From: Serg Melikyan Date: Mon, 18 Mar 2013 13:33:06 +0400 Subject: [PATCH 001/135] Removed obsolete code Removed projects: [windc, windcclient] --- dashboard/windcclient/__init__.py | 0 dashboard/windcclient/common/__init__.py | 0 dashboard/windcclient/common/base.py | 137 -- dashboard/windcclient/common/client.py | 151 --- dashboard/windcclient/common/exceptions.py | 140 -- .../windcclient/common/service_catalog.py | 62 - dashboard/windcclient/common/utils.py | 291 ----- dashboard/windcclient/shell.py | 285 ----- dashboard/windcclient/v1/__init__.py | 0 dashboard/windcclient/v1/client.py | 29 - dashboard/windcclient/v1/datacenters.py | 43 - dashboard/windcclient/v1/services.py | 48 - windc/.gitignore | 2 - windc/README | 8 - windc/bin/windc-api | 94 -- windc/data/CreatePrimaryDC.json | 29 - windc/data/Windows.template | 61 - windc/doc/Makefile | 97 -- windc/doc/source/_static/basic.css | 416 ------ windc/doc/source/_static/default.css | 230 ---- windc/doc/source/_static/jquery.tweet.js | 154 --- windc/doc/source/_static/tweaks.css | 65 - windc/doc/source/_templates/.placeholder | 0 windc/doc/source/_theme/layout.html | 86 -- windc/doc/source/_theme/theme.conf | 5 - windc/doc/source/conf.py | 252 ---- windc/doc/source/index.rst | 53 - windc/etc/windc-api-paste.ini | 57 - windc/etc/windc-api.conf | 34 - windc/heat_run | 8 - windc/openstack/__init__.py | 24 - windc/openstack/common/__init__.py | 19 - windc/openstack/common/config.py | 334 ----- windc/openstack/common/context.py | 40 - windc/openstack/common/exception.py | 143 --- windc/openstack/common/middleware/__init__.py | 0 windc/openstack/common/middleware/context.py | 64 - windc/openstack/common/utils.py | 89 -- windc/openstack/common/wsgi.py | 395 ------ windc/openstack/oldcommon/__init__.py | 19 - windc/openstack/oldcommon/config.py | 337 ----- windc/openstack/oldcommon/context.py | 40 - windc/openstack/oldcommon/exception.py | 147 --- windc/openstack/oldcommon/extensions.py | 538 -------- .../oldcommon/middleware/__init__.py | 0 .../openstack/oldcommon/middleware/context.py | 64 - windc/openstack/oldcommon/utils.py | 210 --- windc/openstack/oldcommon/wsgi.py | 717 ----------- windc/run_tests.log | 3 - windc/run_tests.py | 293 ----- windc/run_tests.sh | 129 -- windc/setup.cfg | 9 - windc/setup.py | 90 -- windc/tests/__init__.py | 0 windc/tests/functional/__init__.py | 0 windc/tests/manual/createDataCenter.sh | 4 - windc/tests/manual/createDataCenterParameters | 7 - windc/tests/manual/createService.sh | 4 - windc/tests/manual/createServiceParameters | 8 - windc/tests/manual/listDataCenter.sh | 1 - windc/tests/unit/__init__.py | 0 windc/tests/unit/test_base_driver.py | 14 - windc/tools/install_venv.py | 154 --- windc/tools/pip-requires | 23 - windc/tools/test-requires | 16 - windc/tools/with_venv.sh | 4 - windc/windc/__init__.py | 0 windc/windc/adapters/openstack.py | 19 - windc/windc/api/__init__.py | 0 windc/windc/api/middleware/__init__.py | 0 windc/windc/api/v1/__init__.py | 0 windc/windc/api/v1/datacenters.py | 76 -- windc/windc/api/v1/filters.py | 42 - windc/windc/api/v1/router.py | 56 - windc/windc/api/v1/services.py | 83 -- windc/windc/api/versions.py | 66 - windc/windc/common/__init__.py | 16 - windc/windc/common/cfg.py | 1135 ----------------- windc/windc/common/client.py | 605 --------- windc/windc/common/config.py | 181 --- windc/windc/common/context.py | 130 -- windc/windc/common/exception.py | 184 --- windc/windc/common/policy.py | 182 --- windc/windc/common/utils.py | 421 ------ windc/windc/common/wsgi.py | 652 ---------- windc/windc/core/__init__.py | 21 - windc/windc/core/api.py | 99 -- windc/windc/core/builder.py | 37 - windc/windc/core/builder_set.py | 72 -- windc/windc/core/builders/ActiveDirectory.py | 210 --- windc/windc/core/builders/DataCenter.py | 37 - windc/windc/core/change_events.py | 55 - windc/windc/core/commands.py | 36 - windc/windc/core/service_status.py | 25 - windc/windc/core/templates.py | 107 -- windc/windc/db/__init__.py | 1 - windc/windc/db/api.py | 164 --- windc/windc/db/base.py | 76 -- windc/windc/db/migrate_repo/README | 4 - windc/windc/db/migrate_repo/__init__.py | 0 windc/windc/db/migrate_repo/manage.py | 5 - windc/windc/db/migrate_repo/migrate.cfg | 25 - .../versions/001_Add_initial_tables.py | 41 - .../db/migrate_repo/versions/__init__.py | 0 windc/windc/db/models.py | 81 -- windc/windc/db/session.py | 122 -- windc/windc/drivers/__init__.py | 0 windc/windc/drivers/command_executor.py | 40 - windc/windc/drivers/openstack_heat.py | 43 - windc/windc/drivers/windows_agent.py | 66 - windc/windc/exception.py | 50 - windc/windc/utils.py | 66 - windc/windc/version.py | 49 - 113 files changed, 12156 deletions(-) delete mode 100644 dashboard/windcclient/__init__.py delete mode 100644 dashboard/windcclient/common/__init__.py delete mode 100644 dashboard/windcclient/common/base.py delete mode 100644 dashboard/windcclient/common/client.py delete mode 100644 dashboard/windcclient/common/exceptions.py delete mode 100644 dashboard/windcclient/common/service_catalog.py delete mode 100644 dashboard/windcclient/common/utils.py delete mode 100644 dashboard/windcclient/shell.py delete mode 100644 dashboard/windcclient/v1/__init__.py delete mode 100644 dashboard/windcclient/v1/client.py delete mode 100644 dashboard/windcclient/v1/datacenters.py delete mode 100644 dashboard/windcclient/v1/services.py delete mode 100644 windc/.gitignore delete mode 100644 windc/README delete mode 100755 windc/bin/windc-api delete mode 100644 windc/data/CreatePrimaryDC.json delete mode 100644 windc/data/Windows.template delete mode 100644 windc/doc/Makefile delete mode 100644 windc/doc/source/_static/basic.css delete mode 100644 windc/doc/source/_static/default.css delete mode 100644 windc/doc/source/_static/jquery.tweet.js delete mode 100644 windc/doc/source/_static/tweaks.css delete mode 100644 windc/doc/source/_templates/.placeholder delete mode 100644 windc/doc/source/_theme/layout.html delete mode 100644 windc/doc/source/_theme/theme.conf delete mode 100644 windc/doc/source/conf.py delete mode 100644 windc/doc/source/index.rst delete mode 100644 windc/etc/windc-api-paste.ini delete mode 100644 windc/etc/windc-api.conf delete mode 100755 windc/heat_run delete mode 100644 windc/openstack/__init__.py delete mode 100644 windc/openstack/common/__init__.py delete mode 100644 windc/openstack/common/config.py delete mode 100644 windc/openstack/common/context.py delete mode 100644 windc/openstack/common/exception.py delete mode 100644 windc/openstack/common/middleware/__init__.py delete mode 100644 windc/openstack/common/middleware/context.py delete mode 100644 windc/openstack/common/utils.py delete mode 100644 windc/openstack/common/wsgi.py delete mode 100644 windc/openstack/oldcommon/__init__.py delete mode 100644 windc/openstack/oldcommon/config.py delete mode 100644 windc/openstack/oldcommon/context.py delete mode 100644 windc/openstack/oldcommon/exception.py delete mode 100644 windc/openstack/oldcommon/extensions.py delete mode 100644 windc/openstack/oldcommon/middleware/__init__.py delete mode 100644 windc/openstack/oldcommon/middleware/context.py delete mode 100644 windc/openstack/oldcommon/utils.py delete mode 100644 windc/openstack/oldcommon/wsgi.py delete mode 100644 windc/run_tests.log delete mode 100644 windc/run_tests.py delete mode 100755 windc/run_tests.sh delete mode 100644 windc/setup.cfg delete mode 100644 windc/setup.py delete mode 100644 windc/tests/__init__.py delete mode 100644 windc/tests/functional/__init__.py delete mode 100755 windc/tests/manual/createDataCenter.sh delete mode 100644 windc/tests/manual/createDataCenterParameters delete mode 100755 windc/tests/manual/createService.sh delete mode 100644 windc/tests/manual/createServiceParameters delete mode 100755 windc/tests/manual/listDataCenter.sh delete mode 100644 windc/tests/unit/__init__.py delete mode 100644 windc/tests/unit/test_base_driver.py delete mode 100644 windc/tools/install_venv.py delete mode 100644 windc/tools/pip-requires delete mode 100644 windc/tools/test-requires delete mode 100755 windc/tools/with_venv.sh delete mode 100644 windc/windc/__init__.py delete mode 100644 windc/windc/adapters/openstack.py delete mode 100644 windc/windc/api/__init__.py delete mode 100644 windc/windc/api/middleware/__init__.py delete mode 100644 windc/windc/api/v1/__init__.py delete mode 100644 windc/windc/api/v1/datacenters.py delete mode 100644 windc/windc/api/v1/filters.py delete mode 100644 windc/windc/api/v1/router.py delete mode 100644 windc/windc/api/v1/services.py delete mode 100644 windc/windc/api/versions.py delete mode 100644 windc/windc/common/__init__.py delete mode 100644 windc/windc/common/cfg.py delete mode 100644 windc/windc/common/client.py delete mode 100644 windc/windc/common/config.py delete mode 100644 windc/windc/common/context.py delete mode 100644 windc/windc/common/exception.py delete mode 100644 windc/windc/common/policy.py delete mode 100644 windc/windc/common/utils.py delete mode 100644 windc/windc/common/wsgi.py delete mode 100644 windc/windc/core/__init__.py delete mode 100644 windc/windc/core/api.py delete mode 100644 windc/windc/core/builder.py delete mode 100644 windc/windc/core/builder_set.py delete mode 100644 windc/windc/core/builders/ActiveDirectory.py delete mode 100644 windc/windc/core/builders/DataCenter.py delete mode 100644 windc/windc/core/change_events.py delete mode 100644 windc/windc/core/commands.py delete mode 100644 windc/windc/core/service_status.py delete mode 100644 windc/windc/core/templates.py delete mode 100644 windc/windc/db/__init__.py delete mode 100644 windc/windc/db/api.py delete mode 100644 windc/windc/db/base.py delete mode 100644 windc/windc/db/migrate_repo/README delete mode 100644 windc/windc/db/migrate_repo/__init__.py delete mode 100644 windc/windc/db/migrate_repo/manage.py delete mode 100644 windc/windc/db/migrate_repo/migrate.cfg delete mode 100644 windc/windc/db/migrate_repo/versions/001_Add_initial_tables.py delete mode 100644 windc/windc/db/migrate_repo/versions/__init__.py delete mode 100644 windc/windc/db/models.py delete mode 100644 windc/windc/db/session.py delete mode 100644 windc/windc/drivers/__init__.py delete mode 100644 windc/windc/drivers/command_executor.py delete mode 100644 windc/windc/drivers/openstack_heat.py delete mode 100644 windc/windc/drivers/windows_agent.py delete mode 100644 windc/windc/exception.py delete mode 100644 windc/windc/utils.py delete mode 100644 windc/windc/version.py diff --git a/dashboard/windcclient/__init__.py b/dashboard/windcclient/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dashboard/windcclient/common/__init__.py b/dashboard/windcclient/common/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dashboard/windcclient/common/base.py b/dashboard/windcclient/common/base.py deleted file mode 100644 index 9f035044..00000000 --- a/dashboard/windcclient/common/base.py +++ /dev/null @@ -1,137 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -""" -Base utilities to build API operation managers and objects on top of. -""" - - -def getid(obj): - """ - Abstracts the common pattern of allowing both an object or an object's ID - (UUID) as a parameter when dealing with relationships. - """ - try: - return obj.id - except AttributeError: - return obj - - -class Manager(object): - """ - Managers interact with a particular type of API and provide CRUD - operations for them. - """ - resource_class = None - - def __init__(self, api): - self.api = api - - def _list(self, url, response_key, obj_class=None, body=None): - resp, body = self.api.client.json_request('GET', url, body=body) - - if obj_class is None: - obj_class = self.resource_class - - data = body[response_key] - return [obj_class(self, res, loaded=True) for res in data if res] - - def _delete(self, url): - self.api.client.raw_request('DELETE', url) - - def _update(self, url, body, response_key=None): - resp, body = self.api.client.json_request('PUT', url, body=body) - # PUT requests may not return a body - if body: - return self.resource_class(self, body[response_key]) - - def _create(self, url, body, response_key, return_raw=False): - resp, body = self.api.client.json_request('POST', url, body=body) - if return_raw: - return body[response_key] - return self.resource_class(self, body[response_key]) - - def _get(self, url, response_key, return_raw=False): - resp, body = self.api.client.json_request('GET', url) - if return_raw: - return body[response_key] - return self.resource_class(self, body[response_key]) - - -class Resource(object): - """ - A resource represents a particular instance of an object (tenant, user, - etc). This is pretty much just a bag for attributes. - - :param manager: Manager object - :param info: dictionary representing resource attributes - :param loaded: prevent lazy-loading if set to True - """ - def __init__(self, manager, info, loaded=False): - self.manager = manager - self._info = info - self._add_details(info) - self._loaded = loaded - - def _add_details(self, info): - for (k, v) in info.iteritems(): - setattr(self, k, v) - - def __getattr__(self, k): - if k not in self.__dict__: - #NOTE(bcwaldon): disallow lazy-loading if already loaded once - if not self.is_loaded(): - self.get() - return self.__getattr__(k) - - raise AttributeError(k) - else: - return self.__dict__[k] - - def __repr__(self): - reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and - k != 'manager') - info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) - return "<%s %s>" % (self.__class__.__name__, info) - - def get_info(self): - if not self.is_loaded(): - self.get() - if self._info: - return self._info.copy() - return {} - - def get(self): - # set_loaded() first ... so if we have to bail, we know we tried. - self.set_loaded(True) - if not hasattr(self.manager, 'get'): - return - - new = self.manager.get(self.id) - if new: - self._info = new._info - self._add_details(new._info) - - def __eq__(self, other): - if not isinstance(other, self.__class__): - return False - if hasattr(self, 'id') and hasattr(other, 'id'): - return self.id == other.id - return self._info == other._info - - def is_loaded(self): - return self._loaded - - def set_loaded(self, val): - self._loaded = val diff --git a/dashboard/windcclient/common/client.py b/dashboard/windcclient/common/client.py deleted file mode 100644 index 1ad4ed84..00000000 --- a/dashboard/windcclient/common/client.py +++ /dev/null @@ -1,151 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# All Rights Reserved -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 -""" -OpenStack Client interface. Handles the REST calls and responses. -""" - -import httplib2 -import copy -import logging -import json - -from . import exceptions -from . import utils -from .service_catalog import ServiceCatalog - - -logger = logging.getLogger(__name__) - - -class HTTPClient(httplib2.Http): - - USER_AGENT = 'python-windcclient' - - def __init__(self, endpoint=None, token=None, username=None, - password=None, tenant_name=None, tenant_id=None, - region_name=None, auth_url=None, auth_tenant_id=None, - timeout=600, insecure=False): - super(HTTPClient, self).__init__(timeout=timeout) - self.endpoint = endpoint - self.auth_token = token - self.auth_url = auth_url - self.auth_tenant_id = auth_tenant_id - self.username = username - self.password = password - self.tenant_name = tenant_name - self.tenant_id = tenant_id - self.region_name = region_name - self.force_exception_to_status_code = True - self.disable_ssl_certificate_validation = insecure - if self.endpoint is None: - self.authenticate() - - def _http_request(self, url, method, **kwargs): - """ Send an http request with the specified characteristics. - """ - - kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) - kwargs['headers'].setdefault('User-Agent', self.USER_AGENT) - if self.auth_token: - kwargs['headers'].setdefault('X-Auth-Token', self.auth_token) - - resp, body = super(HTTPClient, self).request(url, method, **kwargs) - - if logger.isEnabledFor(logging.DEBUG): - utils.http_log(logger, (url, method,), kwargs, resp, body) - - if resp.status in (301, 302, 305): - return self._http_request(resp['location'], method, **kwargs) - - return resp, body - - def _json_request(self, method, url, **kwargs): - """ Wrapper around _http_request to handle setting headers, - JSON enconding/decoding and error handling. - """ - - kwargs.setdefault('headers', {}) - kwargs['headers'].setdefault('Content-Type', 'application/json') - - if 'body' in kwargs and kwargs['body'] is not None: - kwargs['body'] = json.dumps(kwargs['body']) - - resp, body = self._http_request(url, method, **kwargs) - - if body: - try: - body = json.loads(body) - except ValueError: - logger.debug("Could not decode JSON from body: %s" % body) - else: - logger.debug("No body was returned.") - body = None - - if 400 <= resp.status < 600: - # DELETE THIS STRING - logger.exception(url) - raise exceptions.from_response(resp, body) - - return resp, body - - def raw_request(self, method, url, **kwargs): - url = self.endpoint + url - - kwargs.setdefault('headers', {}) - kwargs['headers'].setdefault('Content-Type', - 'application/octet-stream') - - resp, body = self._http_request(url, method, **kwargs) - - if 400 <= resp.status < 600: - logger.exception(url) - raise exceptions.from_response(resp, body) - - return resp, body - - def json_request(self, method, url, **kwargs): - url = self.endpoint + url - resp, body = self._json_request(method, url, **kwargs) - return resp, body - - def authenticate(self): - token_url = self.auth_url + "/tokens" - body = {'auth': {'passwordCredentials': {'username': self.username, - 'password': self.password}}} - if self.tenant_id: - body['auth']['tenantId'] = self.tenant_id - elif self.tenant_name: - body['auth']['tenantName'] = self.tenant_name - - tmp_follow_all_redirects = self.follow_all_redirects - self.follow_all_redirects = True - try: - resp, body = self._json_request('POST', token_url, body=body) - finally: - self.follow_all_redirects = tmp_follow_all_redirects - - try: - self.service_catalog = ServiceCatalog(body['access']) - token = self.service_catalog.get_token() - self.auth_token = token['id'] - self.auth_tenant_id = token['tenant_id'] - except KeyError: - logger.exception("Parse service catalog failed.") - raise exceptions.AuthorizationFailure() - - self.endpoint = self.service_catalog.url_for(attr='region', - filter_value=self.region_name) diff --git a/dashboard/windcclient/common/exceptions.py b/dashboard/windcclient/common/exceptions.py deleted file mode 100644 index 4d17b8db..00000000 --- a/dashboard/windcclient/common/exceptions.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -""" -Exception definitions. -""" - - -class UnsupportedVersion(Exception): - """Indicates that the user is trying to use an unsupported - version of the API""" - pass - - -class CommandError(Exception): - pass - - -class AuthorizationFailure(Exception): - pass - - -class NoUniqueMatch(Exception): - pass - - -class NoTokenLookupException(Exception): - """This form of authentication does not support looking up - endpoints from an existing token.""" - pass - - -class EndpointNotFound(Exception): - """Could not find Service or Region in Service Catalog.""" - pass - - -class AmbiguousEndpoints(Exception): - """Found more than one matching endpoint in Service Catalog.""" - def __init__(self, endpoints=None): - self.endpoints = endpoints - - def __str__(self): - return "AmbiguousEndpoints: %s" % repr(self.endpoints) - - -class ClientException(Exception): - """ - The base exception class for all exceptions this library raises. - """ - def __init__(self, code, message=None, details=None): - self.code = code - self.message = message or self.__class__.message - self.details = details - - def __str__(self): - return "%s (HTTP %s)" % (self.message, self.code) - - -class BadRequest(ClientException): - """ - HTTP 400 - Bad request: you sent some malformed data. - """ - http_status = 400 - message = "Bad request" - - -class Unauthorized(ClientException): - """ - HTTP 401 - Unauthorized: bad credentials. - """ - http_status = 401 - message = "Unauthorized" - - -class Forbidden(ClientException): - """ - HTTP 403 - Forbidden: your credentials don't give you access to this - resource. - """ - http_status = 403 - message = "Forbidden" - - -class NotFound(ClientException): - """ - HTTP 404 - Not found - """ - http_status = 404 - message = "Not found" - - -class OverLimit(ClientException): - """ - HTTP 413 - Over limit: you're over the API limits for this time period. - """ - http_status = 413 - message = "Over limit" - - -# NotImplemented is a python keyword. -class HTTPNotImplemented(ClientException): - """ - HTTP 501 - Not Implemented: the server does not support this operation. - """ - http_status = 501 - message = "Not Implemented" - - -# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__() -# so we can do this: -# _code_map = dict((c.http_status, c) -# for c in ClientException.__subclasses__()) -# -# Instead, we have to hardcode it: -_code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized, - Forbidden, NotFound, OverLimit, HTTPNotImplemented]) - - -def from_response(response, body): - """ - Return an instance of an ClientException or subclass - based on an httplib2 response. - - Usage:: - - resp, body = http.request(...) - if resp.status != 200: - raise exception_from_response(resp, body) - """ - cls = _code_map.get(response.status, ClientException) - if body: - if hasattr(body, 'keys'): - error = body[body.keys()[0]] - message = error.get('message', None) - details = error.get('details', None) - else: - message = 'n/a' - details = body - return cls(code=response.status, message=message, details=details) - else: - return cls(code=response.status) diff --git a/dashboard/windcclient/common/service_catalog.py b/dashboard/windcclient/common/service_catalog.py deleted file mode 100644 index d2a91d67..00000000 --- a/dashboard/windcclient/common/service_catalog.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# Copyright 2011, Piston Cloud Computing, Inc. -# Copyright 2011 Nebula, Inc. -# -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from . import exceptions - - -class ServiceCatalog(object): - """Helper methods for dealing with a Keystone Service Catalog.""" - - def __init__(self, resource_dict): - self.catalog = resource_dict - - def get_token(self): - """Fetch token details fron service catalog""" - token = {'id': self.catalog['token']['id'], - 'expires': self.catalog['token']['expires']} - try: - token['user_id'] = self.catalog['user']['id'] - token['tenant_id'] = self.catalog['token']['tenant']['id'] - except: - # just leave the tenant and user out if it doesn't exist - pass - return token - - def url_for(self, attr=None, filter_value=None, - service_type='loadbalancer', endpoint_type='publicURL'): - """Fetch an endpoint from the service catalog. - - Fetch the specified endpoint from the service catalog for - a particular endpoint attribute. If no attribute is given, return - the first endpoint of the specified type. - - See tests for a sample service catalog. - """ - catalog = self.catalog.get('serviceCatalog', []) - - for service in catalog: - if service['type'] != service_type: - continue - - endpoints = service['endpoints'] - for endpoint in endpoints: - if not filter_value or endpoint.get(attr) == filter_value: - return endpoint[endpoint_type] - - raise exceptions.EndpointNotFound('Endpoint not found.') diff --git a/dashboard/windcclient/common/utils.py b/dashboard/windcclient/common/utils.py deleted file mode 100644 index cabcba88..00000000 --- a/dashboard/windcclient/common/utils.py +++ /dev/null @@ -1,291 +0,0 @@ -import os -import re -import sys -import uuid -import logging -import prettytable - -from . import exceptions - - -def arg(*args, **kwargs): - """Decorator for CLI args.""" - def _decorator(func): - add_arg(func, *args, **kwargs) - return func - return _decorator - - -def env(*vars, **kwargs): - """ - returns the first environment variable set - if none are non-empty, defaults to '' or keyword arg default - """ - for v in vars: - value = os.environ.get(v, None) - if value: - return value - return kwargs.get('default', '') - - -def add_arg(f, *args, **kwargs): - """Bind CLI arguments to a shell.py `do_foo` function.""" - - if not hasattr(f, 'arguments'): - f.arguments = [] - - # NOTE(sirp): avoid dups that can occur when the module is shared across - # tests. - if (args, kwargs) not in f.arguments: - # Because of the sematics of decorator composition if we just append - # to the options list positional options will appear to be backwards. - f.arguments.insert(0, (args, kwargs)) - - -def add_resource_manager_extra_kwargs_hook(f, hook): - """Adds hook to bind CLI arguments to ResourceManager calls. - - The `do_foo` calls in shell.py will receive CLI args and then in turn pass - them through to the ResourceManager. Before passing through the args, the - hooks registered here will be called, giving us a chance to add extra - kwargs (taken from the command-line) to what's passed to the - ResourceManager. - """ - if not hasattr(f, 'resource_manager_kwargs_hooks'): - f.resource_manager_kwargs_hooks = [] - - names = [h.__name__ for h in f.resource_manager_kwargs_hooks] - if hook.__name__ not in names: - f.resource_manager_kwargs_hooks.append(hook) - - -def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False): - """Return extra_kwargs by calling resource manager kwargs hooks.""" - hooks = getattr(f, "resource_manager_kwargs_hooks", []) - extra_kwargs = {} - for hook in hooks: - hook_name = hook.__name__ - hook_kwargs = hook(args) - - conflicting_keys = set(hook_kwargs.keys()) & set(extra_kwargs.keys()) - if conflicting_keys and not allow_conflicts: - raise Exception("Hook '%(hook_name)s' is attempting to redefine" - " attributes '%(conflicting_keys)s'" % locals()) - - extra_kwargs.update(hook_kwargs) - - return extra_kwargs - - -def unauthenticated(f): - """ - Adds 'unauthenticated' attribute to decorated function. - Usage: - @unauthenticated - def mymethod(f): - ... - """ - f.unauthenticated = True - return f - - -def isunauthenticated(f): - """ - Checks to see if the function is marked as not requiring authentication - with the @unauthenticated decorator. Returns True if decorator is - set to True, False otherwise. - """ - return getattr(f, 'unauthenticated', False) - - -def service_type(stype): - """ - Adds 'service_type' attribute to decorated function. - Usage: - @service_type('volume') - def mymethod(f): - ... - """ - def inner(f): - f.service_type = stype - return f - return inner - - -def get_service_type(f): - """ - Retrieves service type from function - """ - return getattr(f, 'service_type', None) - - -def pretty_choice_list(l): - return ', '.join("'%s'" % i for i in l) - - -def print_list(objs, fields, formatters={}, sortby_index=0): - if sortby_index == None: - sortby = None - else: - sortby = fields[sortby_index] - - pt = prettytable.PrettyTable([f for f in fields], caching=False) - pt.align = 'l' - - for o in objs: - row = [] - for field in fields: - if field in formatters: - row.append(formatters[field](o)) - else: - field_name = field.lower().replace(' ', '_') - data = getattr(o, field_name, '') - row.append(data) - pt.add_row(row) - - print pt.get_string(sortby=sortby) - - -def print_flat_list(lst, field): - pt = prettytable.PrettyTable(field) - for el in lst: - pt.add_row([el]) - print pt.get_string() - - -def print_dict(d, property="Property"): - pt = prettytable.PrettyTable([property, 'Value'], caching=False) - pt.align = 'l' - [pt.add_row(list(r)) for r in d.iteritems()] - print pt.get_string(sortby=property) - - -def find_resource(manager, name_or_id): - """Helper for the _find_* methods.""" - # first try to get entity as integer id - try: - if isinstance(name_or_id, int) or name_or_id.isdigit(): - return manager.get(int(name_or_id)) - except exceptions.NotFound: - pass - - # now try to get entity as uuid - try: - uuid.UUID(str(name_or_id)) - return manager.get(name_or_id) - except (ValueError, exceptions.NotFound): - pass - - try: - try: - return manager.find(human_id=name_or_id) - except exceptions.NotFound: - pass - - # finally try to find entity by name - try: - return manager.find(name=name_or_id) - except exceptions.NotFound: - try: - # Volumes does not have name, but display_name - return manager.find(display_name=name_or_id) - except exceptions.NotFound: - msg = "No %s with a name or ID of '%s' exists." % \ - (manager.resource_class.__name__.lower(), name_or_id) - raise exceptions.CommandError(msg) - except exceptions.NoUniqueMatch: - msg = ("Multiple %s matches found for '%s', use an ID to be more" - " specific." % (manager.resource_class.__name__.lower(), - name_or_id)) - raise exceptions.CommandError(msg) - - -def _format_servers_list_networks(server): - output = [] - for (network, addresses) in server.networks.items(): - if len(addresses) == 0: - continue - addresses_csv = ', '.join(addresses) - group = "%s=%s" % (network, addresses_csv) - output.append(group) - - return '; '.join(output) - - -class HookableMixin(object): - """Mixin so classes can register and run hooks.""" - _hooks_map = {} - - @classmethod - def add_hook(cls, hook_type, hook_func): - if hook_type not in cls._hooks_map: - cls._hooks_map[hook_type] = [] - - cls._hooks_map[hook_type].append(hook_func) - - @classmethod - def run_hooks(cls, hook_type, *args, **kwargs): - hook_funcs = cls._hooks_map.get(hook_type) or [] - for hook_func in hook_funcs: - hook_func(*args, **kwargs) - - -def safe_issubclass(*args): - """Like issubclass, but will just return False if not a class.""" - - try: - if issubclass(*args): - return True - except TypeError: - pass - - return False - - -def import_class(import_str): - """Returns a class from a string including module and class.""" - mod_str, _sep, class_str = import_str.rpartition('.') - __import__(mod_str) - return getattr(sys.modules[mod_str], class_str) - -_slugify_strip_re = re.compile(r'[^\w\s-]') -_slugify_hyphenate_re = re.compile(r'[-\s]+') - - -# http://code.activestate.com/recipes/ -# 577257-slugify-make-a-string-usable-in-a-url-or-filename/ -def slugify(value): - """ - Normalizes string, converts to lowercase, removes non-alpha characters, - and converts spaces to hyphens. - - From Django's "django/template/defaultfilters.py". - """ - import unicodedata - if not isinstance(value, unicode): - value = unicode(value) - value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') - value = unicode(_slugify_strip_re.sub('', value).strip().lower()) - return _slugify_hyphenate_re.sub('-', value) - - -def http_log(logger, args, kwargs, resp, body): -# if not logger.isEnabledFor(logging.DEBUG): -# return - - string_parts = ['curl -i'] - for element in args: - if element in ('GET', 'POST'): - string_parts.append(' -X %s' % element) - else: - string_parts.append(' %s' % element) - - for element in kwargs['headers']: - header = ' -H "%s: %s"' % (element, kwargs['headers'][element]) - string_parts.append(header) - - logger.debug("REQ: %s\n" % "".join(string_parts)) - if 'body' in kwargs and kwargs['body']: - logger.debug("REQ BODY: %s\n" % (kwargs['body'])) - logger.debug("RESP:%s\n", resp) - logger.debug("RESP BODY:%s\n", body) diff --git a/dashboard/windcclient/shell.py b/dashboard/windcclient/shell.py deleted file mode 100644 index 196c7a7b..00000000 --- a/dashboard/windcclient/shell.py +++ /dev/null @@ -1,285 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Command-line interface to the OpenStack LBaaS API. -""" - -import argparse -import httplib2 -import os -import sys -import logging - -from balancerclient.common import exceptions as exc -from balancerclient.common import utils -from balancerclient.v1 import shell as shell_v1 - - -LOG = logging.getLogger(__name__) - - -class OpenStackBalancerShell(object): - - def get_base_parser(self): - parser = argparse.ArgumentParser( - prog='balancer', - description=__doc__.strip(), - epilog='See "balancer help COMMAND" ' - 'for help on a specific command.', - add_help=False, - formatter_class=OpenStackHelpFormatter, - ) - - # Global arguments - parser.add_argument('-h', - '--help', - action='store_true', - help=argparse.SUPPRESS) - - parser.add_argument('--debug', - default=False, - action='store_true', - help=argparse.SUPPRESS) - - parser.add_argument('--os_username', - metavar='', - default=utils.env('OS_USERNAME'), - help='Defaults to env[OS_USERNAME]') - - parser.add_argument('--os_password', - metavar='', - default=utils.env('OS_PASSWORD'), - help='Defaults to env[OS_PASSWORD]') - - parser.add_argument('--os_tenant_name', - metavar='', - default=utils.env('OS_TENANT_NAME'), - help='Defaults to env[OS_TENANT_NAME]') - - parser.add_argument('--os_tenant_id', - metavar='', - default=utils.env('OS_TENANT_ID'), - help='Defaults to env[OS_TENANT_ID]') - - parser.add_argument('--os_auth_url', - metavar='', - default=utils.env('OS_AUTH_URL'), - help='Defaults to env[OS_AUTH_URL]') - - parser.add_argument('--os_region_name', - metavar='', - default=utils.env('OS_REGION_NAME'), - help='Defaults to env[OS_REGION_NAME]') - - parser.add_argument('--os_balancer_api_version', - metavar='', - default=utils.env('OS_BALANCER_API_VERSION', - 'KEYSTONE_VERSION'), - help='Defaults to env[OS_BALANCER_API_VERSION]' - ' or 2.0') - - parser.add_argument('--token', - metavar='', - default=utils.env('SERVICE_TOKEN'), - help='Defaults to env[SERVICE_TOKEN]') - - parser.add_argument('--endpoint', - metavar='', - default=utils.env('SERVICE_ENDPOINT'), - help='Defaults to env[SERVICE_ENDPOINT]') - - return parser - - def get_subcommand_parser(self, version): - parser = self.get_base_parser() - - self.subcommands = {} - subparsers = parser.add_subparsers(metavar='') - - try: - actions_module = { - '1': shell_v1, - }[version] - except KeyError: - actions_module = shell_v1 - - self._find_actions(subparsers, actions_module) - self._find_actions(subparsers, self) - - return parser - - def _find_actions(self, subparsers, actions_module): - for attr in (a for a in dir(actions_module) if a.startswith('do_')): - # I prefer to be hypen-separated instead of underscores. - command = attr[3:].replace('_', '-') - callback = getattr(actions_module, attr) - desc = callback.__doc__ or '' - help = desc.strip().split('\n')[0] - arguments = getattr(callback, 'arguments', []) - - subparser = subparsers.add_parser( - command, - help=help, - description=desc, - add_help=False, - formatter_class=OpenStackHelpFormatter) - subparser.add_argument('-h', '--help', action='help', - help=argparse.SUPPRESS) - self.subcommands[command] = subparser - for (args, kwargs) in arguments: - subparser.add_argument(*args, **kwargs) - subparser.set_defaults(func=callback) - - def main(self, argv): - # Parse args once to find version - parser = self.get_base_parser() - (options, args) = parser.parse_known_args(argv) - - # build available subcommands based on version - api_version = options.os_balancer_api_version - subcommand_parser = self.get_subcommand_parser(api_version) - self.parser = subcommand_parser - - # Handle top-level --help/-h before attempting to parse - # a command off the command line - if not argv or options.help: - self.do_help(options) - return 0 - - # Parse args again and call whatever callback was selected - args = subcommand_parser.parse_args(argv) - - # Deal with global arguments - if args.debug: - httplib2.debuglevel = 1 - - # Short-circuit and deal with help command right away. - if args.func == self.do_help: - self.do_help(args) - return 0 - - #FIXME(usrleon): Here should be restrict for project id same as - # for username or apikey but for compatibility it is not. - - if not utils.isunauthenticated(args.func): - # if the user hasn't provided any auth data - if not (args.token or args.endpoint or args.os_username or - args.os_password or args.os_auth_url): - raise exc.CommandError('Expecting authentication method via \n' - ' either a service token, ' - '--token or env[SERVICE_TOKEN], \n' - ' or credentials, ' - '--os_username or env[OS_USERNAME].') - - # if it looks like the user wants to provide a service token - # but is missing something - if args.token or args.endpoint and not ( - args.token and args.endpoint): - if not args.token: - raise exc.CommandError( - 'Expecting a token provided via either --token or ' - 'env[SERVICE_TOKEN]') - - if not args.endpoint: - raise exc.CommandError( - 'Expecting an endpoint provided via either --endpoint ' - 'or env[SERVICE_ENDPOINT]') - - # if it looks like the user wants to provide a credentials - # but is missing something - if ((args.os_username or args.os_password or args.os_auth_url) - and not (args.os_username and args.os_password and - args.os_auth_url)): - if not args.os_username: - raise exc.CommandError( - 'Expecting a username provided via either ' - '--os_username or env[OS_USERNAME]') - - if not args.os_password: - raise exc.CommandError( - 'Expecting a password provided via either ' - '--os_password or env[OS_PASSWORD]') - - if not args.os_auth_url: - raise exc.CommandError( - 'Expecting an auth URL via either --os_auth_url or ' - 'env[OS_AUTH_URL]') - - if utils.isunauthenticated(args.func): - self.cs = shell_generic.CLIENT_CLASS(endpoint=args.os_auth_url) - else: - token = None - endpoint = None - if args.token and args.endpoint: - token = args.token - endpoint = args.endpoint - api_version = options.os_balancer_api_version - self.cs = self.get_api_class(api_version)( - username=args.os_username, - tenant_name=args.os_tenant_name, - tenant_id=args.os_tenant_id, - token=token, - endpoint=endpoint, - password=args.os_password, - auth_url=args.os_auth_url, - region_name=args.os_region_name) - - try: - args.func(self.cs, args) - except exc.Unauthorized: - raise exc.CommandError("Invalid OpenStack LBaaS credentials.") - except exc.AuthorizationFailure: - raise exc.CommandError("Unable to authorize user") - - def get_api_class(self, version): - try: - return { - "1": shell_v1.CLIENT_CLASS, - }[version] - except KeyError: - return shell_v1.CLIENT_CLASS - - @utils.arg('command', metavar='', nargs='?', - help='Display help for ') - def do_help(self, args): - """ - Display help about this program or one of its subcommands. - """ - if getattr(args, 'command', None): - if args.command in self.subcommands: - self.subcommands[args.command].print_help() - else: - raise exc.CommandError("'%s' is not a valid subcommand" % - args.command) - else: - self.parser.print_help() - - -# I'm picky about my shell help. -class OpenStackHelpFormatter(argparse.HelpFormatter): - def start_section(self, heading): - # Title-case the headings - heading = '%s%s' % (heading[0].upper(), heading[1:]) - super(OpenStackHelpFormatter, self).start_section(heading) - - -def main(): - try: - return OpenStackBalancerShell().main(sys.argv[1:]) - except Exception, err: - LOG.exception("The operation executed with an error %r." % err) - raise diff --git a/dashboard/windcclient/v1/__init__.py b/dashboard/windcclient/v1/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/dashboard/windcclient/v1/client.py b/dashboard/windcclient/v1/client.py deleted file mode 100644 index 39287737..00000000 --- a/dashboard/windcclient/v1/client.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# All Rights Reserved -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -from windcclient.common import client -from . import datacenters -from . import services - - -class Client(object): - """Client for the WinDC v1 API.""" - - def __init__(self, **kwargs): - self.client = client.HTTPClient(**kwargs) - self.datacenters = datacenters.DCManager(self) - self.services = services.DCServiceManager(self) diff --git a/dashboard/windcclient/v1/datacenters.py b/dashboard/windcclient/v1/datacenters.py deleted file mode 100644 index 3d184f1a..00000000 --- a/dashboard/windcclient/v1/datacenters.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# All Rights Reserved -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -from windcclient.common import base - - -class DC(base.Resource): - - def __repr__(self): - return "" % self._info - - -class DCManager(base.Manager): - resource_class = DC - - def list(self): - return self._list('/datacenters', 'datacenters') - - def create(self, name, **extra): - body = {'name': name, 'services': {}} - body.update(extra) - return self._create('/datacenters', body, 'datacenter') - - def delete(self, datacenter_id): - return self._delete("/datacenters/%s" % datacenter_id) - - def get(self, datacenter_id): - return self._get("/datacenters/%s" % datacenter_id, - 'datacenter') diff --git a/dashboard/windcclient/v1/services.py b/dashboard/windcclient/v1/services.py deleted file mode 100644 index 809a30c9..00000000 --- a/dashboard/windcclient/v1/services.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# All Rights Reserved -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -from windcclient.common import base - - -class DCService(base.Resource): - - def __repr__(self): - return "" % self._info - - -class DCServiceManager(base.Manager): - resource_class = DCService - - def list(self, datacenter): - return self._list("/datacenters/%s/services" % base.getid(datacenter), - 'services') - - def create(self, datacenter, parameters): - body = {'dc_count': 1,} - body.update(parameters) - return self._create("/datacenters/%s/services" % base.getid(datacenter), - body, 'service') - - def delete(self, datacenter_id, service_id): - return self._delete("/datacenters/%s/services/%s" % \ - (datacenter_id, service_id)) - - def get(self, datacenter, service): - return self._get("/datacenters/%s/services/%s" % \ - (base.getid(datacenter), - base.getid(service)), - 'service') diff --git a/windc/.gitignore b/windc/.gitignore deleted file mode 100644 index b9489855..00000000 --- a/windc/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.swp -*.pyc diff --git a/windc/README b/windc/README deleted file mode 100644 index 1def0120..00000000 --- a/windc/README +++ /dev/null @@ -1,8 +0,0 @@ -This is the Windows DataCenter project. It serves two main purposes: - -* Proof the proposed architecture for windows dc service -* Provide a Demo for the Windows Environment Management features - -This is not a final project. It is a POC for the Demo and architecture verification purposes. - - diff --git a/windc/bin/windc-api b/windc/bin/windc-api deleted file mode 100755 index 6c8adc03..00000000 --- a/windc/bin/windc-api +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env python -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Windows DataCenter API Server -""" - -import optparse -import os -import sys -import gettext -# If ../windc/__init__.py exists, add ../ to Python search path, so that -# it will override what happens to be installed in /usr/(local/)lib/python... -possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), - os.pardir, - os.pardir)) -if os.path.exists(os.path.join(possible_topdir, 'windc', '__init__.py')): - sys.path.insert(0, possible_topdir) - sys.path.insert(0, '.') - - -from windc.common import cfg -from windc.common import config -from windc.common import wsgi -from windc.db import session -from windc.core import builder_set - -gettext.install('balancer', unicode=1) - -dbsync_opt = cfg.BoolOpt('dbsync', default=False, - help='Perform database schema synchronization') - -if __name__ == '__main__': - try: - conf = config.WindcConfigOpts() - conf.register_cli_opt(dbsync_opt) - conf() - - config.setup_logging(conf) - if conf.dbsync: - session.sync(conf) - else: - builder_set.builders.load(conf) - app = config.load_paste_app(conf) - server = wsgi.Server() - server.start(app, conf, default_port=8181) - server.wait() - except RuntimeError, e: - sys.exit("ERROR: %s" % e) - - - - - -# def create_options(parser): -# """ -# Sets up the CLI and config-file options that may be -# parsed and program commands. -# -# :param parser: The option parser -# """ -# config.add_common_options(parser) -# config.add_log_options(parser) -# -# -# if __name__ == '__main__': -# oparser = optparse.OptionParser(version='%%prog %s' -# % version.version_string()) -# create_options(oparser) -# (options, args) = config.parse_options(oparser) -# -# # try: -# conf, app = config.load_paste_app('windc', options, args) -# -# server = wsgi.Server() -# server.start(app, int(conf['bind_port']), conf['bind_host']) -# server.wait() -# # except RuntimeError, e: -# # sys.exit("ERROR: %s" % e) diff --git a/windc/data/CreatePrimaryDC.json b/windc/data/CreatePrimaryDC.json deleted file mode 100644 index fb74fbbc..00000000 --- a/windc/data/CreatePrimaryDC.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "Scripts": [ - "RnVuY3Rpb24gU2V0LUxvY2FsVXNlclBhc3N3b3JkIHsNCiAgICBwYXJhbSAoDQogICAgICAgIFtTdHJpbmddICRVc2VyTmFtZSwNCiAgICAgICAgW1N0cmluZ10gJFBhc3N3b3JkLA0KICAgICAgICBbU3dpdGNoXSAkRm9yY2UNCiAgICApDQogICAgDQogICAgdHJhcCB7IFN0b3AtRXhlY3V0aW9uICRfIH0NCiAgICANCiAgICBpZiAoKEdldC1XbWlPYmplY3QgV2luMzJfVXNlckFjY291bnQgLUZpbHRlciAiTG9jYWxBY2NvdW50ID0gJ1RydWUnIEFORCBOYW1lPSckVXNlck5hbWUnIikgLWVxICRudWxsKSB7DQogICAgICAgIHRocm93ICJVbmFibGUgdG8gZmluZCBsb2NhbCB1c2VyIGFjY291bnQgJyRVc2VyTmFtZSciDQogICAgfQ0KICAgIA0KICAgIGlmICgkRm9yY2UpIHsNCiAgICAgICAgV3JpdGUtTG9nICJDaGFuZ2luZyBwYXNzd29yZCBmb3IgdXNlciAnJFVzZXJOYW1lJyB0byAnKioqKionIiAjIDopDQogICAgICAgIChbQURTSV0gIldpbk5UOi8vLi8kVXNlck5hbWUiKS5TZXRQYXNzd29yZCgkUGFzc3dvcmQpDQogICAgfQ0KICAgIGVsc2Ugew0KICAgICAgICBXcml0ZS1Mb2dXYXJuaW5nICJZb3UgYXJlIHRyeWluZyB0byBjaGFuZ2UgcGFzc3dvcmQgZm9yIHVzZXIgJyRVc2VyTmFtZScuIFRvIGRvIHRoaXMgcGxlYXNlIHJ1biB0aGUgY29tbWFuZCBhZ2FpbiB3aXRoIC1Gb3JjZSBwYXJhbWV0ZXIuIg0KICAgICAgICAkVXNlckFjY291bnQNCiAgICB9DQp9DQoNCg0KDQpGdW5jdGlvbiBJbnN0YWxsLVJvbGVQcmltYXJ5RG9tYWluQ29udHJvbGxlcg0Kew0KPCMNCi5TWU5PUFNJUw0KQ29uZmlndXJlIG5vZGUncyBuZXR3b3JrIGFkYXB0ZXJzLg0KQ3JlYXRlIGZpcnN0IGRvbWFpbiBjb250cm9sbGVyIGluIHRoZSBmb3Jlc3QuDQoNCi5FWEFNUExFDQpQUz4gSW5zdGFsbC1Sb2xlUHJpbWFyeURvbWFpbkNvbnRyb2xsZXIgLURvbWFpbk5hbWUgYWNtZS5sb2NhbCAtU2FmZU1vZGVQYXNzd29yZCAiUEBzc3cwcmQiDQoNCkluc3RhbGwgRE5TIGFuZCBBRERTLCBjcmVhdGUgZm9yZXN0IGFuZCBkb21haW4gJ2FjbWUubG9jYWwnLg0KU2V0IERDIHJlY292ZXJ5IG1vZGUgcGFzc3dvcmQgdG8gJ1BAc3N3MHJkJy4NCiM+DQoJDQoJcGFyYW0NCgkoDQoJCVtTdHJpbmddDQoJCSMgTmV3IGRvbWFpbiBuYW1lLg0KCQkkRG9tYWluTmFtZSwNCgkJDQoJCVtTdHJpbmddDQoJCSMgRG9tYWluIGNvbnRyb2xsZXIgcmVjb3ZlcnkgbW9kZSBwYXNzd29yZC4NCgkJJFNhZmVNb2RlUGFzc3dvcmQNCgkpDQoNCgl0cmFwIHsgU3RvcC1FeGVjdXRpb24gJF8gfQ0KDQogICAgICAgICMgQWRkIHJlcXVpcmVkIHdpbmRvd3MgZmVhdHVyZXMNCglBZGQtV2luZG93c0ZlYXR1cmVXcmFwcGVyIGANCgkJLU5hbWUgIkROUyIsIkFELURvbWFpbi1TZXJ2aWNlcyIsIlJTQVQtREZTLU1nbXQtQ29uIiBgDQoJCS1JbmNsdWRlTWFuYWdlbWVudFRvb2xzIGANCiAgICAgICAgLU5vdGlmeVJlc3RhcnQNCg0KDQoJV3JpdGUtTG9nICJDcmVhdGluZyBmaXJzdCBkb21haW4gY29udHJvbGxlciAuLi4iDQoJCQ0KCSRTTUFQID0gQ29udmVydFRvLVNlY3VyZVN0cmluZyAtU3RyaW5nICRTYWZlTW9kZVBhc3N3b3JkIC1Bc1BsYWluVGV4dCAtRm9yY2UNCgkJDQoJSW5zdGFsbC1BRERTRm9yZXN0IGANCgkJLURvbWFpbk5hbWUgJERvbWFpbk5hbWUgYA0KCQktU2FmZU1vZGVBZG1pbmlzdHJhdG9yUGFzc3dvcmQgJFNNQVAgYA0KCQktRG9tYWluTW9kZSBEZWZhdWx0IGANCgkJLUZvcmVzdE1vZGUgRGVmYXVsdCBgDQoJCS1Ob1JlYm9vdE9uQ29tcGxldGlvbiBgDQoJCS1Gb3JjZSBgDQoJCS1FcnJvckFjdGlvbiBTdG9wIHwgT3V0LU51bGwNCg0KCVdyaXRlLUhvc3QgIldhaXRpbmcgZm9yIHJlYm9vdCAuLi4iCQkNCiMJU3RvcC1FeGVjdXRpb24gLUV4aXRDb2RlIDMwMTAgLUV4aXRTdHJpbmcgIkNvbXB1dGVyIG11c3QgYmUgcmVzdGFydGVkIHRvIGZpbmlzaCBkb21haW4gY29udHJvbGxlciBwcm9tb3Rpb24uIg0KIwlXcml0ZS1Mb2cgIlJlc3RhcmluZyBjb21wdXRlciAuLi4iDQojCVJlc3RhcnQtQ29tcHV0ZXIgLUZvcmNlDQp9DQo=" - ], - "Commands": [ - { - "Name": "Import-Module", - "Arguments": { - "Name": "CoreFunctions" - } - }, - { - "Name": "Set-LocalUserPassword", - "Arguments": { - "UserName": "Administrator", - "Password": "@adm_password", - "Force": true - } - }, - { - "Name": "Install-RolePrimaryDomainController", - "Arguments": { - "DomainName": "@dc_name", - "SafeModePassword": "@recovery_password" - } - } - ], - "RebootOnCompletion": 1 -} \ No newline at end of file diff --git a/windc/data/Windows.template b/windc/data/Windows.template deleted file mode 100644 index 66d650d6..00000000 --- a/windc/data/Windows.template +++ /dev/null @@ -1,61 +0,0 @@ -{ - "AWSTemplateFormatVersion" : "2010-09-09", - - "Description" : "", - - "Parameters" : { - "KeyName" : { - "Description" : "Name of an existing Amazon EC2 key pair for RDP access", - "Type" : "String", - "Default" : "keero_key" - }, - "InstanceType" : { - "Description" : "Amazon EC2 instance type", - "Type" : "String", - "Default" : "m1.medium", - "AllowedValues" : [ "m1.small", "m1.medium", "m1.large" ] - }, - "ImageName" : { - "Description" : "Image name", - "Type" : "String", - "Default" : "ws-2012-full-agent", - "AllowedValues" : [ "ws-2012-full", "ws-2012-core", "ws-2012-full-agent" ] - } - }, - - "Resources" : { - "IAMUser" : { - "Type" : "AWS::IAM::User", - "Properties" : { - "Path": "/", - "Policies": [{ - "PolicyName": "root", - "PolicyDocument": { "Statement":[{ - "Effect": "Allow", - "Action": "CloudFormation:DescribeStackResource", - "Resource": "*" - }]} - }] - } - }, - - "IAMUserAccessKey" : { - "Type" : "AWS::IAM::AccessKey", - "Properties" : { - "UserName" : {"Ref": "IAMUser"} - } - }, - - "InstanceTemplate": { - "Type" : "AWS::EC2::Instance", - "Properties": { - "InstanceType" : { "Ref" : "InstanceType" }, - "ImageId" : { "Ref" : "ImageName" }, - "KeyName" : { "Ref" : "KeyName" } - } - } - }, - - "Outputs" : { - } -} \ No newline at end of file diff --git a/windc/doc/Makefile b/windc/doc/Makefile deleted file mode 100644 index 251a008e..00000000 --- a/windc/doc/Makefile +++ /dev/null @@ -1,97 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXSOURCE = source -PAPER = -BUILDDIR = build - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SPHINXSOURCE) - -.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest - -.DEFAULT_GOAL = html - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -rm -rf nova.sqlite - if [ -f .autogenerated ] ; then \ - cat .autogenerated | xargs rm ; \ - rm .autogenerated ; \ - fi - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/nova.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/nova.qhc" - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ - "run these through (pdf)latex." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." diff --git a/windc/doc/source/_static/basic.css b/windc/doc/source/_static/basic.css deleted file mode 100644 index d909ce37..00000000 --- a/windc/doc/source/_static/basic.css +++ /dev/null @@ -1,416 +0,0 @@ -/** - * Sphinx stylesheet -- basic theme - * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - */ - -/* -- main layout ----------------------------------------------------------- */ - -div.clearer { - clear: both; -} - -/* -- relbar ---------------------------------------------------------------- */ - -div.related { - width: 100%; - font-size: 90%; -} - -div.related h3 { - display: none; -} - -div.related ul { - margin: 0; - padding: 0 0 0 10px; - list-style: none; -} - -div.related li { - display: inline; -} - -div.related li.right { - float: right; - margin-right: 5px; -} - -/* -- sidebar --------------------------------------------------------------- */ - -div.sphinxsidebarwrapper { - padding: 10px 5px 0 10px; -} - -div.sphinxsidebar { - float: left; - width: 230px; - margin-left: -100%; - font-size: 90%; -} - -div.sphinxsidebar ul { - list-style: none; -} - -div.sphinxsidebar ul ul, -div.sphinxsidebar ul.want-points { - margin-left: 20px; - list-style: square; -} - -div.sphinxsidebar ul ul { - margin-top: 0; - margin-bottom: 0; -} - -div.sphinxsidebar form { - margin-top: 10px; -} - -div.sphinxsidebar input { - border: 1px solid #98dbcc; - font-family: sans-serif; - font-size: 1em; -} - -img { - border: 0; -} - -/* -- search page ----------------------------------------------------------- */ - -ul.search { - margin: 10px 0 0 20px; - padding: 0; -} - -ul.search li { - padding: 5px 0 5px 20px; - background-image: url(file.png); - background-repeat: no-repeat; - background-position: 0 7px; -} - -ul.search li a { - font-weight: bold; -} - -ul.search li div.context { - color: #888; - margin: 2px 0 0 30px; - text-align: left; -} - -ul.keywordmatches li.goodmatch a { - font-weight: bold; -} - -/* -- index page ------------------------------------------------------------ */ - -table.contentstable { - width: 90%; -} - -table.contentstable p.biglink { - line-height: 150%; -} - -a.biglink { - font-size: 1.3em; -} - -span.linkdescr { - font-style: italic; - padding-top: 5px; - font-size: 90%; -} - -/* -- general index --------------------------------------------------------- */ - -table.indextable td { - text-align: left; - vertical-align: top; -} - -table.indextable dl, table.indextable dd { - margin-top: 0; - margin-bottom: 0; -} - -table.indextable tr.pcap { - height: 10px; -} - -table.indextable tr.cap { - margin-top: 10px; - background-color: #f2f2f2; -} - -img.toggler { - margin-right: 3px; - margin-top: 3px; - cursor: pointer; -} - -/* -- general body styles --------------------------------------------------- */ - -a.headerlink { - visibility: hidden; -} - -h1:hover > a.headerlink, -h2:hover > a.headerlink, -h3:hover > a.headerlink, -h4:hover > a.headerlink, -h5:hover > a.headerlink, -h6:hover > a.headerlink, -dt:hover > a.headerlink { - visibility: visible; -} - -div.body p.caption { - text-align: inherit; -} - -div.body td { - text-align: left; -} - -.field-list ul { - padding-left: 1em; -} - -.first { -} - -p.rubric { - margin-top: 30px; - font-weight: bold; -} - -/* -- sidebars -------------------------------------------------------------- */ - -div.sidebar { - margin: 0 0 0.5em 1em; - border: 1px solid #ddb; - padding: 7px 7px 0 7px; - background-color: #ffe; - width: 40%; - float: right; -} - -p.sidebar-title { - font-weight: bold; -} - -/* -- topics ---------------------------------------------------------------- */ - -div.topic { - border: 1px solid #ccc; - padding: 7px 7px 0 7px; - margin: 10px 0 10px 0; -} - -p.topic-title { - font-size: 1.1em; - font-weight: bold; - margin-top: 10px; -} - -/* -- admonitions ----------------------------------------------------------- */ - -div.admonition { - margin-top: 10px; - margin-bottom: 10px; - padding: 7px; -} - -div.admonition dt { - font-weight: bold; -} - -div.admonition dl { - margin-bottom: 0; -} - -p.admonition-title { - margin: 0px 10px 5px 0px; - font-weight: bold; -} - -div.body p.centered { - text-align: center; - margin-top: 25px; -} - -/* -- tables ---------------------------------------------------------------- */ - -table.docutils { - border: 0; - border-collapse: collapse; -} - -table.docutils td, table.docutils th { - padding: 1px 8px 1px 0; - border-top: 0; - border-left: 0; - border-right: 0; - border-bottom: 1px solid #aaa; -} - -table.field-list td, table.field-list th { - border: 0 !important; -} - -table.footnote td, table.footnote th { - border: 0 !important; -} - -th { - text-align: left; - padding-right: 5px; -} - -/* -- other body styles ----------------------------------------------------- */ - -dl { - margin-bottom: 15px; -} - -dd p { - margin-top: 0px; -} - -dd ul, dd table { - margin-bottom: 10px; -} - -dd { - margin-top: 3px; - margin-bottom: 10px; - margin-left: 30px; -} - -dt:target, .highlight { - background-color: #fbe54e; -} - -dl.glossary dt { - font-weight: bold; - font-size: 1.1em; -} - -.field-list ul { - margin: 0; - padding-left: 1em; -} - -.field-list p { - margin: 0; -} - -.refcount { - color: #060; -} - -.optional { - font-size: 1.3em; -} - -.versionmodified { - font-style: italic; -} - -.system-message { - background-color: #fda; - padding: 5px; - border: 3px solid red; -} - -.footnote:target { - background-color: #ffa -} - -.line-block { - display: block; - margin-top: 1em; - margin-bottom: 1em; -} - -.line-block .line-block { - margin-top: 0; - margin-bottom: 0; - margin-left: 1.5em; -} - -/* -- code displays --------------------------------------------------------- */ - -pre { - overflow: auto; -} - -td.linenos pre { - padding: 5px 0px; - border: 0; - background-color: transparent; - color: #aaa; -} - -table.highlighttable { - margin-left: 0.5em; -} - -table.highlighttable td { - padding: 0 0.5em 0 0.5em; -} - -tt.descname { - background-color: transparent; - font-weight: bold; - font-size: 1.2em; -} - -tt.descclassname { - background-color: transparent; -} - -tt.xref, a tt { - background-color: transparent; - font-weight: bold; -} - -h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { - background-color: transparent; -} - -/* -- math display ---------------------------------------------------------- */ - -img.math { - vertical-align: middle; -} - -div.body div.math p { - text-align: center; -} - -span.eqno { - float: right; -} - -/* -- printout stylesheet --------------------------------------------------- */ - -@media print { - div.document, - div.documentwrapper, - div.bodywrapper { - margin: 0 !important; - width: 100%; - } - - div.sphinxsidebar, - div.related, - div.footer, - #top-link { - display: none; - } -} diff --git a/windc/doc/source/_static/default.css b/windc/doc/source/_static/default.css deleted file mode 100644 index c8091ecb..00000000 --- a/windc/doc/source/_static/default.css +++ /dev/null @@ -1,230 +0,0 @@ -/** - * Sphinx stylesheet -- default theme - * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - */ - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: sans-serif; - font-size: 100%; - background-color: #11303d; - color: #000; - margin: 0; - padding: 0; -} - -div.document { - background-color: #1c4e63; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 0 0 0 230px; -} - -div.body { - background-color: #ffffff; - color: #000000; - padding: 0 20px 30px 20px; -} - -div.footer { - color: #ffffff; - width: 100%; - padding: 9px 0 9px 0; - text-align: center; - font-size: 75%; -} - -div.footer a { - color: #ffffff; - text-decoration: underline; -} - -div.related { - background-color: #133f52; - line-height: 30px; - color: #ffffff; -} - -div.related a { - color: #ffffff; -} - -div.sphinxsidebar { -} - -div.sphinxsidebar h3 { - font-family: 'Trebuchet MS', sans-serif; - color: #ffffff; - font-size: 1.4em; - font-weight: normal; - margin: 0; - padding: 0; -} - -div.sphinxsidebar h3 a { - color: #ffffff; -} - -div.sphinxsidebar h4 { - font-family: 'Trebuchet MS', sans-serif; - color: #ffffff; - font-size: 1.3em; - font-weight: normal; - margin: 5px 0 0 0; - padding: 0; -} - -div.sphinxsidebar p { - color: #ffffff; -} - -div.sphinxsidebar p.topless { - margin: 5px 10px 10px 10px; -} - -div.sphinxsidebar ul { - margin: 10px; - padding: 0; - color: #ffffff; -} - -div.sphinxsidebar a { - color: #98dbcc; -} - -div.sphinxsidebar input { - border: 1px solid #98dbcc; - font-family: sans-serif; - font-size: 1em; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #355f7c; - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -div.body p, div.body dd, div.body li { - text-align: left; - line-height: 130%; -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: 'Trebuchet MS', sans-serif; - background-color: #f2f2f2; - font-weight: normal; - color: #20435c; - border-bottom: 1px solid #ccc; - margin: 20px -20px 10px -20px; - padding: 3px 0 3px 10px; -} - -div.body h1 { margin-top: 0; font-size: 200%; } -div.body h2 { font-size: 160%; } -div.body h3 { font-size: 140%; } -div.body h4 { font-size: 120%; } -div.body h5 { font-size: 110%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: #c60f0f; - font-size: 0.8em; - padding: 0 4px 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - background-color: #c60f0f; - color: white; -} - -div.body p, div.body dd, div.body li { - text-align: left; - line-height: 130%; -} - -div.admonition p.admonition-title + p { - display: inline; -} - -div.admonition p { - margin-bottom: 5px; -} - -div.admonition pre { - margin-bottom: 5px; -} - -div.admonition ul, div.admonition ol { - margin-bottom: 5px; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -div.warning { - background-color: #ffe4e4; - border: 1px solid #f66; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre { - padding: 5px; - background-color: #eeffcc; - color: #333333; - line-height: 120%; - border: 1px solid #ac9; - border-left: none; - border-right: none; -} - -tt { - background-color: #ecf0f3; - padding: 0 1px 0 1px; - font-size: 0.95em; -} - -.warning tt { - background: #efc2c2; -} - -.note tt { - background: #d6d6d6; -} diff --git a/windc/doc/source/_static/jquery.tweet.js b/windc/doc/source/_static/jquery.tweet.js deleted file mode 100644 index c93fea87..00000000 --- a/windc/doc/source/_static/jquery.tweet.js +++ /dev/null @@ -1,154 +0,0 @@ -(function($) { - - $.fn.tweet = function(o){ - var s = { - username: ["seaofclouds"], // [string] required, unless you want to display our tweets. :) it can be an array, just do ["username1","username2","etc"] - list: null, //[string] optional name of list belonging to username - avatar_size: null, // [integer] height and width of avatar if displayed (48px max) - count: 3, // [integer] how many tweets to display? - intro_text: null, // [string] do you want text BEFORE your your tweets? - outro_text: null, // [string] do you want text AFTER your tweets? - join_text: null, // [string] optional text in between date and tweet, try setting to "auto" - auto_join_text_default: "i said,", // [string] auto text for non verb: "i said" bullocks - auto_join_text_ed: "i", // [string] auto text for past tense: "i" surfed - auto_join_text_ing: "i am", // [string] auto tense for present tense: "i was" surfing - auto_join_text_reply: "i replied to", // [string] auto tense for replies: "i replied to" @someone "with" - auto_join_text_url: "i was looking at", // [string] auto tense for urls: "i was looking at" http:... - loading_text: null, // [string] optional loading text, displayed while tweets load - query: null // [string] optional search query - }; - - if(o) $.extend(s, o); - - $.fn.extend({ - linkUrl: function() { - var returning = []; - var regexp = /((ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?)/gi; - this.each(function() { - returning.push(this.replace(regexp,"$1")); - }); - return $(returning); - }, - linkUser: function() { - var returning = []; - var regexp = /[\@]+([A-Za-z0-9-_]+)/gi; - this.each(function() { - returning.push(this.replace(regexp,"@$1")); - }); - return $(returning); - }, - linkHash: function() { - var returning = []; - var regexp = / [\#]+([A-Za-z0-9-_]+)/gi; - this.each(function() { - returning.push(this.replace(regexp, ' #$1')); - }); - return $(returning); - }, - capAwesome: function() { - var returning = []; - this.each(function() { - returning.push(this.replace(/\b(awesome)\b/gi, '$1')); - }); - return $(returning); - }, - capEpic: function() { - var returning = []; - this.each(function() { - returning.push(this.replace(/\b(epic)\b/gi, '$1')); - }); - return $(returning); - }, - makeHeart: function() { - var returning = []; - this.each(function() { - returning.push(this.replace(/(<)+[3]/gi, "")); - }); - return $(returning); - } - }); - - function relative_time(time_value) { - var parsed_date = Date.parse(time_value); - var relative_to = (arguments.length > 1) ? arguments[1] : new Date(); - var delta = parseInt((relative_to.getTime() - parsed_date) / 1000); - var pluralize = function (singular, n) { - return '' + n + ' ' + singular + (n == 1 ? '' : 's'); - }; - if(delta < 60) { - return 'less than a minute ago'; - } else if(delta < (45*60)) { - return 'about ' + pluralize("minute", parseInt(delta / 60)) + ' ago'; - } else if(delta < (24*60*60)) { - return 'about ' + pluralize("hour", parseInt(delta / 3600)) + ' ago'; - } else { - return 'about ' + pluralize("day", parseInt(delta / 86400)) + ' ago'; - } - } - - function build_url() { - var proto = ('https:' == document.location.protocol ? 'https:' : 'http:'); - if (s.list) { - return proto+"//api.twitter.com/1/"+s.username[0]+"/lists/"+s.list+"/statuses.json?per_page="+s.count+"&callback=?"; - } else if (s.query == null && s.username.length == 1) { - return proto+'//twitter.com/status/user_timeline/'+s.username[0]+'.json?count='+s.count+'&callback=?'; - } else { - var query = (s.query || 'from:'+s.username.join('%20OR%20from:')); - return proto+'//search.twitter.com/search.json?&q='+query+'&rpp='+s.count+'&callback=?'; - } - } - - return this.each(function(){ - var list = $('
    ').appendTo(this); - var intro = '

    '+s.intro_text+'

    '; - var outro = '

    '+s.outro_text+'

    '; - var loading = $('

    '+s.loading_text+'

    '); - - if(typeof(s.username) == "string"){ - s.username = [s.username]; - } - - if (s.loading_text) $(this).append(loading); - $.getJSON(build_url(), function(data){ - if (s.loading_text) loading.remove(); - if (s.intro_text) list.before(intro); - $.each((data.results || data), function(i,item){ - // auto join text based on verb tense and content - if (s.join_text == "auto") { - if (item.text.match(/^(@([A-Za-z0-9-_]+)) .*/i)) { - var join_text = s.auto_join_text_reply; - } else if (item.text.match(/(^\w+:\/\/[A-Za-z0-9-_]+\.[A-Za-z0-9-_:%&\?\/.=]+) .*/i)) { - var join_text = s.auto_join_text_url; - } else if (item.text.match(/^((\w+ed)|just) .*/im)) { - var join_text = s.auto_join_text_ed; - } else if (item.text.match(/^(\w*ing) .*/i)) { - var join_text = s.auto_join_text_ing; - } else { - var join_text = s.auto_join_text_default; - } - } else { - var join_text = s.join_text; - }; - - var from_user = item.from_user || item.user.screen_name; - var profile_image_url = item.profile_image_url || item.user.profile_image_url; - var join_template = ' '+join_text+' '; - var join = ((s.join_text) ? join_template : ' '); - var avatar_template = ''+from_user+'\'s avatar'; - var avatar = (s.avatar_size ? avatar_template : ''); - var date = ''+relative_time(item.created_at)+''; - var text = '' +$([item.text]).linkUrl().linkUser().linkHash().makeHeart().capAwesome().capEpic()[0]+ ''; - - // until we create a template option, arrange the items below to alter a tweet's display. - list.append('
  • ' + avatar + date + join + text + '
  • '); - - list.children('li:first').addClass('tweet_first'); - list.children('li:odd').addClass('tweet_even'); - list.children('li:even').addClass('tweet_odd'); - }); - if (s.outro_text) list.after(outro); - }); - - }); - }; -})(jQuery); \ No newline at end of file diff --git a/windc/doc/source/_static/tweaks.css b/windc/doc/source/_static/tweaks.css deleted file mode 100644 index 16cd6e76..00000000 --- a/windc/doc/source/_static/tweaks.css +++ /dev/null @@ -1,65 +0,0 @@ -ul.todo_list { - list-style-type: none; - margin: 0; - padding: 0; -} - -ul.todo_list li { - display: block; - margin: 0; - padding: 7px 0; - border-top: 1px solid #eee; -} - -ul.todo_list li p { - display: inline; -} - -ul.todo_list li p.link { - font-weight: bold; -} - -ul.todo_list li p.details { - font-style: italic; -} - -ul.todo_list li { -} - -div.admonition { - border: 1px solid #8F1000; -} - -div.admonition p.admonition-title { - background-color: #8F1000; - border-bottom: 1px solid #8E8E8E; -} - -a { - color: #CF2F19; -} - -div.related ul li a { - color: #CF2F19; -} - -div.sphinxsidebar h4 { - background-color:#8E8E8E; - border:1px solid #255E6E; - color:white; - font-size:1em; - margin:1em 0 0.5em; - padding:0.1em 0 0.1em 0.5em; -} - -em { - font-style: normal; -} - -table.docutils { - font-size: 11px; -} - -a tt { - color:#CF2F19; -} \ No newline at end of file diff --git a/windc/doc/source/_templates/.placeholder b/windc/doc/source/_templates/.placeholder deleted file mode 100644 index e69de29b..00000000 diff --git a/windc/doc/source/_theme/layout.html b/windc/doc/source/_theme/layout.html deleted file mode 100644 index e3eb54b7..00000000 --- a/windc/doc/source/_theme/layout.html +++ /dev/null @@ -1,86 +0,0 @@ -{% extends "sphinxdoc/layout.html" %} -{% set css_files = css_files + ['_static/tweaks.css'] %} -{% set script_files = script_files + ['_static/jquery.tweet.js'] %} -{% block extrahead %} - -{% endblock %} - -{%- macro sidebar() %} - {%- if not embedded %}{% if not theme_nosidebar|tobool %} -
    -
    - {%- block sidebarlogo %} - {%- if logo %} - - {%- endif %} - {%- endblock %} - {%- block sidebartoc %} - {%- if display_toc %} -

    {{ _('Table Of Contents') }}

    - {{ toc }} - {%- endif %} - {%- endblock %} - {%- block sidebarrel %} - {%- if prev %} -

    {{ _('Previous topic') }}

    -

    {{ prev.title }}

    - {%- endif %} - {%- if next %} -

    {{ _('Next topic') }}

    -

    {{ next.title }}

    - {%- endif %} - {%- endblock %} - {%- block sidebarsourcelink %} - {%- if show_source and has_source and sourcename %} -

    {{ _('This Page') }}

    - - {%- endif %} - {%- endblock %} - {%- if customsidebar %} - {% include customsidebar %} - {%- endif %} - {%- block sidebarsearch %} - {%- if pagename != "search" %} - - - {%- endif %} - - {%- if pagename == "index" %} -

    {{ _('Twitter Feed') }}

    - - {%- endif %} - - - {%- endblock %} -
    -
    - {%- endif %}{% endif %} -{%- endmacro %} diff --git a/windc/doc/source/_theme/theme.conf b/windc/doc/source/_theme/theme.conf deleted file mode 100644 index e039fe01..00000000 --- a/windc/doc/source/_theme/theme.conf +++ /dev/null @@ -1,5 +0,0 @@ -[theme] -inherit = sphinxdoc -stylesheet = sphinxdoc.css -pygments_style = friendly - diff --git a/windc/doc/source/conf.py b/windc/doc/source/conf.py deleted file mode 100644 index cca6b7fc..00000000 --- a/windc/doc/source/conf.py +++ /dev/null @@ -1,252 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2011 OpenStack, LLC. -# -# 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. - -# -# Skeleton documentation build configuration file, created by -# sphinx-quickstart on Tue May 18 13:50:15 2010. -# -# This file is execfile()'d with the current directory set to it's containing -# dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import os -import sys - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.append([os.path.abspath('../skeleton'), - os.path.abspath('..'), - os.path.abspath('../bin') - ]) - -# -- 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.coverage', - 'sphinx.ext.ifconfig', - 'sphinx.ext.intersphinx', - 'sphinx.ext.pngmath', - 'sphinx.ext.graphviz', - 'sphinx.ext.todo'] - -todo_include_todos = True - -# Add any paths that contain templates here, relative to this directory. -templates_path = [] -if os.getenv('HUDSON_PUBLISH_DOCS'): - templates_path = ['_ga', '_templates'] -else: - templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'Skeleton' -copyright = u'2011-present, OpenStack, LLC.' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -from skeleton import version as skeleton_version -# The full version, including alpha/beta/rc tags. -release = skeleton_version.version_string() -# The short X.Y version. -version = skeleton_version.canonical_version_string() - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of documents that shouldn't be included in the build. -#unused_docs = [] - -# List of directories, relative to source directory, that shouldn't be searched -# for source files. -exclude_trees = [] - -# The reST default role (for this markup: `text`) to use for all documents. -#default_role = None - -# 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 - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -show_authors = True - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -modindex_common_prefix = ['skeleton.'] - -# -- Options for man page output -------------------------------------------- - -# Grouping the document tree for man pages. -# List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual' - -man_pages = [ - ('man/skeletonapi', 'skeleton-api', u'Skeleton API Server', - [u'OpenStack'], 1), - ('man/skeletonregistry', 'skeleton-registry', u'Skeleton Registry Server', - [u'OpenStack'], 1), - ('man/skeletonmanage', 'skeleton-manage', u'Skeleton Management Utility', - [u'OpenStack'], 1) - ] - - -# -- 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' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = ['_theme'] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_use_modindex = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'skeletondoc' - - -# -- Options for LaTeX output ------------------------------------------------ - -# The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' - -# The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, -# documentclass [howto/manual]). -latex_documents = [ - ('index', 'Skeleton.tex', u'Skeleton Documentation', - u'Skeleton Team', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# Additional stuff for the LaTeX preamble. -#latex_preamble = '' - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_use_modindex = True - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'python': ('http://docs.python.org/', None), - 'dashboard': ('http://dashboard.openstack.org', None), - 'glance': ('http://glance.openstack.org', None), - 'keystone': ('http://keystone.openstack.org', None), - 'nova': ('http://nova.openstack.org', None), - 'swift': ('http://swift.openstack.org', None)} diff --git a/windc/doc/source/index.rst b/windc/doc/source/index.rst deleted file mode 100644 index a103a897..00000000 --- a/windc/doc/source/index.rst +++ /dev/null @@ -1,53 +0,0 @@ -.. - Copyright 2011 OpenStack, LLC. - All Rights Reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); you may - not use this file except in compliance with the License. You may obtain - a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - License for the specific language governing permissions and limitations - under the License. - -Welcome to Skeleton's documentation! -=================================== - -Description of Skeleton project - -Concepts -======== - -.. toctree:: - :maxdepth: 1 - -Using Skeleton -============== - -.. toctree:: - :maxdepth: 1 - - gettingstarted - installing - -Developer Docs -============== - -.. toctree:: - :maxdepth: 1 - -Outstanding Documentation Tasks -=============================== - -.. todolist:: - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/windc/etc/windc-api-paste.ini b/windc/etc/windc-api-paste.ini deleted file mode 100644 index 8dbdc8c2..00000000 --- a/windc/etc/windc-api-paste.ini +++ /dev/null @@ -1,57 +0,0 @@ -[DEFAULT] -# Show more verbose log output (sets INFO log level output) -verbose = True -# Show debugging output in logs (sets DEBUG log level output) -debug = True -# Address to bind the server to -bind_host = 0.0.0.0 -# Port the bind the server to -bind_port = 8082 -# Log to this file. Make sure the user running skeleton-api has -# permissions to write to this file! -log_file = /tmp/api.log -# Orchestration Adapter Section -# -#provider - Cloud provider to use (openstack, amazon, dummy) -provider = openstack - -# Heat specific parameters -#heat_url - url for the heat service -# [auto] - find in the keystone -heat_url = auto - -#heat_api_version - version of the API to use -# -heat_api_version = 1 - - -[pipeline:windc-api] -pipeline = apiv1app -# NOTE: use the following pipeline for keystone -#pipeline = authtoken context apiv1app - -[app:apiv1app] -paste.app_factory = windc.common.wsgi:app_factory -windc.app_factory = windc.api.v1.router:API - -[filter:context] -paste.filter_factory = windc.common.wsgi:filter_factory -windc.filter_factory = windc.common.context:ContextMiddleware - -[filter:authtoken] -paste.filter_factory = keystone.middleware.auth_token:filter_factory -auth_host = 172.18.67.57 -auth_port = 35357 -auth_protocol = http -auth_uri = http://172.18.67.57:5000/v2.0/ -admin_tenant_name = service -admin_user = windc -admin_password = 000 - -[filter:auth-context] -paste.filter_factory = windc.common.wsgi:filter_factory -windc.filter_factory = keystone.middleware.balancer_auth_token:KeystoneContextMiddleware - -[rabbitmq] -host = 10.0.0.1 -vhost = keero \ No newline at end of file diff --git a/windc/etc/windc-api.conf b/windc/etc/windc-api.conf deleted file mode 100644 index 3f1381b5..00000000 --- a/windc/etc/windc-api.conf +++ /dev/null @@ -1,34 +0,0 @@ -[DEFAULT] -# Show more verbose log output (sets INFO log level output) -verbose = True - -# Show debugging output in logs (sets DEBUG log level output) -debug = True - -# Address to bind the server to -bind_host = 0.0.0.0 - -# Port the bind the server to -bind_port = 8082 - -# Log to this file. Make sure the user running skeleton-api has -# permissions to write to this file! -log_file = /tmp/api.log - -[pipeline:windc-api] -pipeline = versionnegotiation context apiv1app - -[pipeline:versions] -pipeline = versionsapp - -[app:versionsapp] -paste.app_factory = windc.api.versions:app_factory - -[app:apiv1app] -paste.app_factory = windc.api.v1:app_factory - -[filter:versionnegotiation] -paste.filter_factory = windc.api.middleware.version_negotiation:filter_factory - -[filter:context] -paste.filter_factory = openstack.common.middleware.context:filter_factory diff --git a/windc/heat_run b/windc/heat_run deleted file mode 100755 index e2498a84..00000000 --- a/windc/heat_run +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - - -#export OS_USERNAME=admin -#source ../../devstack/openrc -#nova keypair-add keero-linux-keys > heat_key.priv -heat "$@" - diff --git a/windc/openstack/__init__.py b/windc/openstack/__init__.py deleted file mode 100644 index 77531382..00000000 --- a/windc/openstack/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -# This ensures the openstack namespace is defined -try: - import pkg_resources - pkg_resources.declare_namespace(__name__) -except ImportError: - import pkgutil - __path__ = pkgutil.extend_path(__path__, __name__) diff --git a/windc/openstack/common/__init__.py b/windc/openstack/common/__init__.py deleted file mode 100644 index b22496f0..00000000 --- a/windc/openstack/common/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -# TODO(jaypipes) Code in this module is intended to be ported to the eventual -# openstack-common library diff --git a/windc/openstack/common/config.py b/windc/openstack/common/config.py deleted file mode 100644 index 74301a8c..00000000 --- a/windc/openstack/common/config.py +++ /dev/null @@ -1,334 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Routines for configuring Openstack Projects -""" - -import ConfigParser -import logging -import logging.config -import logging.handlers -import optparse -import os -import re -import sys - -from paste import deploy - -DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s" -DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" - - -def parse_options(parser, cli_args=None): - """ - Returns the parsed CLI options, command to run and its arguments, merged - with any same-named options found in a configuration file. - - The function returns a tuple of (options, args), where options is a - mapping of option key/str(value) pairs, and args is the set of arguments - (not options) supplied on the command-line. - - The reason that the option values are returned as strings only is that - ConfigParser and paste.deploy only accept string values... - - :param parser: The option parser - :param cli_args: (Optional) Set of arguments to process. If not present, - sys.argv[1:] is used. - :retval tuple of (options, args) - """ - - (options, args) = parser.parse_args(cli_args) - - return (vars(options), args) - - -def add_common_options(parser): - """ - Given a supplied optparse.OptionParser, adds an OptionGroup that - represents all common configuration options. - - :param parser: optparse.OptionParser - """ - help_text = "The following configuration options are common to "\ - "this app's programs." - - group = optparse.OptionGroup(parser, "Common Options", help_text) - group.add_option('-v', '--verbose', default=False, dest="verbose", - action="store_true", - help="Print more verbose output") - group.add_option('-d', '--debug', default=False, dest="debug", - action="store_true", - help="Print debugging output") - group.add_option('--config-file', default=None, metavar="PATH", - help="Path to the config file to use. When not specified " - "(the default), we generally look at the first " - "argument specified to be a config file, and if " - "that is also missing, we search standard " - "directories for a config file.") - parser.add_option_group(group) - - -def add_log_options(parser): - """ - Given a supplied optparse.OptionParser, adds an OptionGroup that - represents all the configuration options around logging. - - :param parser: optparse.OptionParser - """ - help_text = "The following configuration options are specific to logging "\ - "functionality for this program." - - group = optparse.OptionGroup(parser, "Logging Options", help_text) - group.add_option('--log-config', default=None, metavar="PATH", - help="If this option is specified, the logging " - "configuration file specified is used and overrides " - "any other logging options specified. Please see " - "the Python logging module documentation for " - "details on logging configuration files.") - group.add_option('--log-date-format', metavar="FORMAT", - default=DEFAULT_LOG_DATE_FORMAT, - help="Format string for %(asctime)s in log records. " - "Default: %default") - group.add_option('--log-file', default=None, metavar="PATH", - help="(Optional) Name of log file to output to. " - "If not set, logging will go to stdout.") - group.add_option("--log-dir", default=None, - help="(Optional) The directory to keep log files in " - "(will be prepended to --logfile)") - group.add_option('--use-syslog', default=False, dest="use_syslog", - action="store_true", - help="Use syslog for logging.") - parser.add_option_group(group) - - -def setup_logging(options, conf): - """ - Sets up the logging options for a log with supplied name - - :param options: Mapping of typed option key/values - :param conf: Mapping of untyped key/values from config file - """ - - if options.get('log_config', None): - # Use a logging configuration file for all settings... - if os.path.exists(options['log_config']): - logging.config.fileConfig(options['log_config']) - return - else: - raise RuntimeError("Unable to locate specified logging " - "config file: %s" % options['log_config']) - - # If either the CLI option or the conf value - # is True, we set to True - debug = options.get('debug') or \ - get_option(conf, 'debug', type='bool', default=False) - verbose = options.get('verbose') or \ - get_option(conf, 'verbose', type='bool', default=False) - root_logger = logging.root - if debug: - root_logger.setLevel(logging.DEBUG) - elif verbose: - root_logger.setLevel(logging.INFO) - else: - root_logger.setLevel(logging.WARNING) - - # Set log configuration from options... - # Note that we use a hard-coded log format in the options - # because of Paste.Deploy bug #379 - # http://trac.pythonpaste.org/pythonpaste/ticket/379 - log_format = options.get('log_format', DEFAULT_LOG_FORMAT) - log_date_format = options.get('log_date_format', DEFAULT_LOG_DATE_FORMAT) - formatter = logging.Formatter(log_format, log_date_format) - - logfile = options.get('log_file') - if not logfile: - logfile = conf.get('log_file') - - use_syslog = options.get('use_syslog') or \ - get_option(conf, 'use_syslog', type='bool', default=False) - - if use_syslog: - handler = logging.handlers.SysLogHandler(address='/dev/log') - elif logfile: - logdir = options.get('log_dir') - if not logdir: - logdir = conf.get('log_dir') - if logdir: - logfile = os.path.join(logdir, logfile) - handler = logging.FileHandler(logfile) - else: - handler = logging.StreamHandler(sys.stdout) - - handler.setFormatter(formatter) - root_logger.addHandler(handler) - - -def find_config_file(app_name, options, args, config_dir=None): - """ - Return the first config file found for an application. - - We search for the paste config file in the following order: - * If --config-file option is used, use that - * If args[0] is a file, use that - * Search for $app.conf in standard directories: - * . - * ~.config_dir/ - * ~ - * /etc/config_dir - * /etc - - :retval Full path to config file, or None if no config file found - """ - config_dir = config_dir or app_name - - fix_path = lambda p: os.path.abspath(os.path.expanduser(p)) - if options.get('config_file'): - if os.path.exists(options['config_file']): - return fix_path(options['config_file']) - elif args: - if os.path.exists(args[0]): - return fix_path(args[0]) - - # Handle standard directory search for $app_name.conf - config_file_dirs = [fix_path(os.getcwd()), - fix_path(os.path.join('~', '.' + config_dir)), - fix_path('~'), - os.path.join('/etc', config_dir), - '/etc'] - - for cfg_dir in config_file_dirs: - cfg_file = os.path.join(cfg_dir, '%s.conf' % app_name) - if os.path.exists(cfg_file): - return cfg_file - - -def load_paste_config(app_name, options, args, config_dir=None): - """ - Looks for a config file to use for an app and returns the - config file path and a configuration mapping from a paste config file. - - We search for the paste config file in the following order: - * If --config-file option is used, use that - * If args[0] is a file, use that - * Search for $app_name.conf in standard directories: - * . - * ~.config_dir/ - * ~ - * /etc/config_dir - * /etc - - :param app_name: Name of the application to load config for, or None. - None signifies to only load the [DEFAULT] section of - the config file. - :param options: Set of typed options returned from parse_options() - :param args: Command line arguments from argv[1:] - :retval Tuple of (conf_file, conf) - - :raises RuntimeError when config file cannot be located or there was a - problem loading the configuration file. - """ - conf_file = find_config_file(app_name, options, args, config_dir) - if not conf_file: - raise RuntimeError("Unable to locate any configuration file. " - "Cannot load application %s" % app_name) - try: - app = wsgi.paste_deploy_app(conf_file, app_name, conf) - conf = deploy.appconfig("config:%s" % conf_file, name=app_name) - return conf_file, conf - except Exception, e: - raise RuntimeError("Error trying to load config %s: %s" - % (conf_file, e)) - - -def load_paste_app(app_name, options, args, config_dir=None): - """ - Builds and returns a WSGI app from a paste config file. - - We search for the paste config file in the following order: - * If --config-file option is used, use that - * If args[0] is a file, use that - * Search for $app_name.conf in standard directories: - * . - * ~.config_dir/ - * ~ - * /etc/config_dir - * /etc - - :param app_name: Name of the application to load - :param options: Set of typed options returned from parse_options() - :param args: Command line arguments from argv[1:] - - :raises RuntimeError when config file cannot be located or application - cannot be loaded from config file - """ - conf_file, conf = load_paste_config(app_name, options, - args, config_dir) - - try: - # Setup logging early, supplying both the CLI options and the - # configuration mapping from the config file - setup_logging(options, conf) - - # We only update the conf dict for the verbose and debug - # flags. Everything else must be set up in the conf file... - debug = options.get('debug') or \ - get_option(conf, 'debug', type='bool', default=False) - verbose = options.get('verbose') or \ - get_option(conf, 'verbose', type='bool', default=False) - conf['debug'] = debug - conf['verbose'] = verbose - - # Log the options used when starting if we're in debug mode... - if debug: - logger = logging.getLogger(app_name) - logger.debug("*" * 80) - logger.debug("Configuration options gathered from config file:") - logger.debug(conf_file) - logger.debug("================================================") - items = dict([(k, v) for k, v in conf.items() - if k not in ('__file__', 'here')]) - for key, value in sorted(items.items()): - logger.debug("%(key)-30s %(value)s" % locals()) - logger.debug("*" * 80) - app = deploy.loadapp("config:%s" % conf_file, name=app_name) - except (LookupError, ImportError), e: - raise RuntimeError("Unable to load %(app_name)s from " - "configuration file %(conf_file)s." - "\nGot: %(e)r" % locals()) - return conf, app - - -def get_option(options, option, **kwargs): - if option in options: - value = options[option] - type_ = kwargs.get('type', 'str') - if type_ == 'bool': - if hasattr(value, 'lower'): - return value.lower() == 'true' - else: - return value - elif type_ == 'int': - return int(value) - elif type_ == 'float': - return float(value) - else: - return value - elif 'default' in kwargs: - return kwargs['default'] - else: - raise KeyError("option '%s' not found" % option) diff --git a/windc/openstack/common/context.py b/windc/openstack/common/context.py deleted file mode 100644 index a9a16f8e..00000000 --- a/windc/openstack/common/context.py +++ /dev/null @@ -1,40 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Simple class that stores security context information in the web request. - -Projects should subclass this class if they wish to enhance the request -context or provide additional information in their specific WSGI pipeline. -""" - - -class RequestContext(object): - - """ - Stores information about the security context under which the user - accesses the system, as well as additional request information. - """ - - def __init__(self, auth_tok=None, user=None, tenant=None, is_admin=False, - read_only=False, show_deleted=False): - self.auth_tok = auth_tok - self.user = user - self.tenant = tenant - self.is_admin = is_admin - self.read_only = read_only - self.show_deleted = show_deleted diff --git a/windc/openstack/common/exception.py b/windc/openstack/common/exception.py deleted file mode 100644 index a81355ef..00000000 --- a/windc/openstack/common/exception.py +++ /dev/null @@ -1,143 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Exceptions common to OpenStack projects -""" - -import logging - - -class ProcessExecutionError(IOError): - def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None, - description=None): - if description is None: - description = "Unexpected error while running command." - if exit_code is None: - exit_code = '-' - message = "%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r" % ( - description, cmd, exit_code, stdout, stderr) - IOError.__init__(self, message) - - -class Error(Exception): - def __init__(self, message=None): - super(Error, self).__init__(message) - - -class ApiError(Error): - def __init__(self, message='Unknown', code='Unknown'): - self.message = message - self.code = code - super(ApiError, self).__init__('%s: %s' % (code, message)) - - -class NotFound(Error): - pass - - -class UnknownScheme(Error): - - msg = "Unknown scheme '%s' found in URI" - - def __init__(self, scheme): - msg = self.__class__.msg % scheme - super(UnknownScheme, self).__init__(msg) - - -class BadStoreUri(Error): - - msg = "The Store URI %s was malformed. Reason: %s" - - def __init__(self, uri, reason): - msg = self.__class__.msg % (uri, reason) - super(BadStoreUri, self).__init__(msg) - - -class Duplicate(Error): - pass - - -class NotAuthorized(Error): - pass - - -class NotEmpty(Error): - pass - - -class Invalid(Error): - pass - - -class BadInputError(Exception): - """Error resulting from a client sending bad input to a server""" - pass - - -class MissingArgumentError(Error): - pass - - -class DatabaseMigrationError(Error): - pass - - -class ClientConnectionError(Exception): - """Error resulting from a client connecting to a server""" - pass - - -def wrap_exception(f): - def _wrap(*args, **kw): - try: - return f(*args, **kw) - except Exception, e: - if not isinstance(e, Error): - #exc_type, exc_value, exc_traceback = sys.exc_info() - logging.exception('Uncaught exception') - #logging.error(traceback.extract_stack(exc_traceback)) - raise Error(str(e)) - raise - _wrap.func_name = f.func_name - return _wrap - - -class OpenstackException(Exception): - """ - Base Exception - - To correctly use this class, inherit from it and define - a 'message' property. That message will get printf'd - with the keyword arguments provided to the constructor. - """ - message = "An unknown exception occurred" - - def __init__(self, **kwargs): - try: - self._error_string = self.message % kwargs - - except Exception: - # at least get the core message out if something happened - self._error_string = self.message - - def __str__(self): - return self._error_string - - -class InvalidContentType(OpenstackException): - message = "Invalid content type %(content_type)s" diff --git a/windc/openstack/common/middleware/__init__.py b/windc/openstack/common/middleware/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/windc/openstack/common/middleware/context.py b/windc/openstack/common/middleware/context.py deleted file mode 100644 index be7dafee..00000000 --- a/windc/openstack/common/middleware/context.py +++ /dev/null @@ -1,64 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Middleware that attaches a context to the WSGI request -""" - -from openstack.common import utils -from openstack.common import wsgi -from openstack.common import context - - -class ContextMiddleware(wsgi.Middleware): - def __init__(self, app, options): - self.options = options - super(ContextMiddleware, self).__init__(app) - - def make_context(self, *args, **kwargs): - """ - Create a context with the given arguments. - """ - - # Determine the context class to use - ctxcls = context.RequestContext - if 'context_class' in self.options: - ctxcls = utils.import_class(self.options['context_class']) - - return ctxcls(*args, **kwargs) - - def process_request(self, req): - """ - Extract any authentication information in the request and - construct an appropriate context from it. - """ - # Use the default empty context, with admin turned on for - # backwards compatibility - req.context = self.make_context(is_admin=True) - - -def filter_factory(global_conf, **local_conf): - """ - Factory method for paste.deploy - """ - conf = global_conf.copy() - conf.update(local_conf) - - def filter(app): - return ContextMiddleware(app, conf) - - return filter diff --git a/windc/openstack/common/utils.py b/windc/openstack/common/utils.py deleted file mode 100644 index 0d2f89e0..00000000 --- a/windc/openstack/common/utils.py +++ /dev/null @@ -1,89 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -System-level utilities and helper functions. -""" - -import datetime -import sys - -from openstack.common import exception - - -TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" - - -def int_from_bool_as_string(subject): - """ - Interpret a string as a boolean and return either 1 or 0. - - Any string value in: - ('True', 'true', 'On', 'on', '1') - is interpreted as a boolean True. - - Useful for JSON-decoded stuff and config file parsing - """ - return bool_from_string(subject) and 1 or 0 - - -def bool_from_string(subject): - """ - Interpret a string as a boolean. - - Any string value in: - ('True', 'true', 'On', 'on', '1') - is interpreted as a boolean True. - - Useful for JSON-decoded stuff and config file parsing - """ - if type(subject) == type(bool): - return subject - if hasattr(subject, 'startswith'): # str or unicode... - if subject.strip().lower() in ('true', 'on', '1'): - return True - return False - - -def import_class(import_str): - """Returns a class from a string including module and class""" - mod_str, _sep, class_str = import_str.rpartition('.') - try: - __import__(mod_str) - return getattr(sys.modules[mod_str], class_str) - except (ImportError, ValueError, AttributeError): - raise exception.NotFound('Class %s cannot be found' % class_str) - - -def import_object(import_str): - """Returns an object including a module or module and class""" - try: - __import__(import_str) - return sys.modules[import_str] - except ImportError: - cls = import_class(import_str) - return cls() - - -def isotime(at=None): - if not at: - at = datetime.datetime.utcnow() - return at.strftime(TIME_FORMAT) - - -def parse_isotime(timestr): - return datetime.datetime.strptime(timestr, TIME_FORMAT) diff --git a/windc/openstack/common/wsgi.py b/windc/openstack/common/wsgi.py deleted file mode 100644 index e6d7b2ef..00000000 --- a/windc/openstack/common/wsgi.py +++ /dev/null @@ -1,395 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Utility methods for working with WSGI servers -""" - -import json -import logging -import sys -import datetime -import urllib2 - -import eventlet -import eventlet.wsgi -eventlet.patcher.monkey_patch(all=False, socket=True) -import routes -import routes.middleware -import webob.dec -import webob.exc - -from openstack.common import exception - -logger = logging.getLogger('openstack.common.wsgi') - - -class WritableLogger(object): - """A thin wrapper that responds to `write` and logs.""" - - def __init__(self, logger, level=logging.DEBUG): - self.logger = logger - self.level = level - - def write(self, msg): - self.logger.log(self.level, msg.strip("\n")) - - -def run_server(application, port): - """Run a WSGI server with the given application.""" - sock = eventlet.listen(('0.0.0.0', port)) - eventlet.wsgi.server(sock, application) - - -class Server(object): - """Server class to manage multiple WSGI sockets and applications.""" - - def __init__(self, threads=1000): - self.pool = eventlet.GreenPool(threads) - - def start(self, application, port, host='0.0.0.0', backlog=128): - """Run a WSGI server with the given application.""" - socket = eventlet.listen((host, port), backlog=backlog) - self.pool.spawn_n(self._run, application, socket) - - def wait(self): - """Wait until all servers have completed running.""" - try: - self.pool.waitall() - except KeyboardInterrupt: - pass - - def _run(self, application, socket): - """Start a WSGI server in a new green thread.""" - logger = logging.getLogger('eventlet.wsgi.server') - eventlet.wsgi.server(socket, application, custom_pool=self.pool, - log=WritableLogger(logger)) - - -class Middleware(object): - """ - Base WSGI middleware wrapper. These classes require an application to be - initialized that will be called next. By default the middleware will - simply call its wrapped app, or you can override __call__ to customize its - behavior. - """ - - def __init__(self, application): - self.application = application - - def process_request(self, req): - """ - Called on each request. - - If this returns None, the next application down the stack will be - executed. If it returns a response then that response will be returned - and execution will stop here. - """ - return None - - def process_response(self, response): - """Do whatever you'd like to the response.""" - return response - - @webob.dec.wsgify - def __call__(self, req): - response = self.process_request(req) - if response: - return response - response = req.get_response(self.application) - return self.process_response(response) - - -class Debug(Middleware): - """ - Helper class that can be inserted into any WSGI application chain - to get information about the request and response. - """ - - @webob.dec.wsgify - def __call__(self, req): - print ("*" * 40) + " REQUEST ENVIRON" - for key, value in req.environ.items(): - print key, "=", value - print - resp = req.get_response(self.application) - - print ("*" * 40) + " RESPONSE HEADERS" - for (key, value) in resp.headers.iteritems(): - print key, "=", value - print - - resp.app_iter = self.print_generator(resp.app_iter) - - return resp - - @staticmethod - def print_generator(app_iter): - """ - Iterator that prints the contents of a wrapper string iterator - when iterated. - """ - print ("*" * 40) + " BODY" - for part in app_iter: - sys.stdout.write(part) - sys.stdout.flush() - yield part - print - - -class Router(object): - - """ - WSGI middleware that maps incoming requests to WSGI apps. - """ - - def __init__(self, mapper): - """ - Create a router for the given routes.Mapper. - - Each route in `mapper` must specify a 'controller', which is a - WSGI app to call. You'll probably want to specify an 'action' as - well and have your controller be a wsgi.Controller, who will route - the request to the action method. - - Examples: - mapper = routes.Mapper() - sc = ServerController() - - # Explicit mapping of one route to a controller+action - mapper.connect(None, "/svrlist", controller=sc, action="list") - - # Actions are all implicitly defined - mapper.resource("server", "servers", controller=sc) - - # Pointing to an arbitrary WSGI app. You can specify the - # {path_info:.*} parameter so the target app can be handed just that - # section of the URL. - mapper.connect(None, "/v1.0/{path_info:.*}", controller=BlogApp()) - """ - self.map = mapper - self._router = routes.middleware.RoutesMiddleware(self._dispatch, - self.map) - - @webob.dec.wsgify - def __call__(self, req): - """ - Route the incoming request to a controller based on self.map. - If no match, return a 404. - """ - return self._router - - @staticmethod - @webob.dec.wsgify - def _dispatch(req): - """ - Called by self._router after matching the incoming request to a route - and putting the information into req.environ. Either returns 404 - or the routed WSGI app's response. - """ - match = req.environ['wsgiorg.routing_args'][1] - if not match: - return webob.exc.HTTPNotFound() - app = match['controller'] - return app - - -class Request(webob.Request): - - """Add some Openstack API-specific logic to the base webob.Request.""" - - def best_match_content_type(self): - """Determine the requested response content-type.""" - supported = ('application/json',) - bm = self.accept.best_match(supported) - return bm or 'application/json' - - def get_content_type(self, allowed_content_types): - """Determine content type of the request body.""" - if not "Content-Type" in self.headers: - raise exception.InvalidContentType(content_type=None) - - content_type = self.content_type - - if content_type not in allowed_content_types: - raise exception.InvalidContentType(content_type=content_type) - else: - return content_type - - -class JSONRequestDeserializer(object): - def has_body(self, request): - """ - Returns whether a Webob.Request object will possess an entity body. - - :param request: Webob.Request object - """ - if 'transfer-encoding' in request.headers: - return True - elif request.content_length > 0: - return True - - return False - - def from_json(self, datastring): - return json.loads(datastring) - - def default(self, request): - msg = "Request deserialization: %s" % request - logger.debug(msg) - if self.has_body(request): - logger.debug("Deserialization: request has body") - if request.headers['Content-Type'] == 'application/x-www-form-urlencoded': - body = urllib2.unquote(request.body) - else: - body = request.body - msg = "Request body: %s" % body - logger.debug(msg) - return {'body': self.from_json(body)} - else: - logger.debug("Deserialization: request has NOT body") - return {} - - -class JSONResponseSerializer(object): - - def to_json(self, data): - def sanitizer(obj): - if isinstance(obj, datetime.datetime): - return obj.isoformat() - return obj - - return json.dumps(data, default=sanitizer) - - def default(self, response, result): - logger.debug("JSONSerializer default method called.") - response.headers['Content-Type'] = 'application/json' - response.body = self.to_json(result) - - -class Resource(object): - """ - WSGI app that handles (de)serialization and controller dispatch. - - Reads routing information supplied by RoutesMiddleware and calls - the requested action method upon its deserializer, controller, - and serializer. Those three objects may implement any of the basic - controller action methods (create, update, show, index, delete) - along with any that may be specified in the api router. A 'default' - method may also be implemented to be used in place of any - non-implemented actions. Deserializer methods must accept a request - argument and return a dictionary. Controller methods must accept a - request argument. Additionally, they must also accept keyword - arguments that represent the keys returned by the Deserializer. They - may raise a webob.exc exception or return a dict, which will be - serialized by requested content type. - """ - def __init__(self, controller, deserializer, serializer): - """ - :param controller: object that implement methods created by routes lib - :param deserializer: object that supports webob request deserialization - through controller-like actions - :param serializer: object that supports webob response serialization - through controller-like actions - """ - self.controller = controller - self.serializer = serializer - self.deserializer = deserializer - - # NOTE(yorik-sar): ugly fix for Routes misbehaviour - def __add__(self, other): - return other - - @webob.dec.wsgify(RequestClass=Request) - def __call__(self, request): - """WSGI method that controls (de)serialization and method dispatch.""" - logger.debug("Resource __call__ is invoked") - action_args = self.get_action_args(request.environ) - action = action_args.pop('action', None) - - deserialized_params = self.deserialize_request(action, request) - action_args.update(deserialized_params) - action_result = self.execute_action(action, request, **action_args) - - try: - return self.serialize_response(action, action_result, request) - - # return unserializable result (typically a webob exc) - except Exception: - return action_result - - def deserialize_request(self, action, request): - return self.dispatch(self.deserializer, action, request) - - def serialize_response(self, action, action_result, request): - msg = "Called serialize response Action:%s Result:%s Request:%s" % (action, action_result, request) - logger.debug(msg) - - try: - if not self.controller: - meth = getattr(self, action) - else: - meth = getattr(self.controller, action) - except AttributeError: - raise - - code = 200 - if hasattr(meth, 'wsgi_code'): - code = meth.wsgi_code - - response = webob.Response() - response.status = code - logger.debug("serializer: dispatching call") - #TODO check why it fails with original openstack code - #self.dispatch(self.serializer, action, response, - # action_result, request) - if action_result is not None: - self.serializer.default(response, action_result) - msg = "Response: %s" % response - logger.debug(msg) - return response - - def execute_action(self, action, request, **action_args): - return self.dispatch(self.controller, action, request, **action_args) - - def dispatch(self, obj, action, *args, **kwargs): - """Find action-specific method on self and call it.""" - try: - method = getattr(obj, action) - except AttributeError: - method = getattr(obj, 'default') - - return method(*args, **kwargs) - - def get_action_args(self, request_environment): - """Parse dictionary created by routes library.""" - try: - args = request_environment['wsgiorg.routing_args'][1].copy() - except Exception: - return {} - - try: - del args['controller'] - except KeyError: - pass - - try: - del args['format'] - except KeyError: - pass - - return args diff --git a/windc/openstack/oldcommon/__init__.py b/windc/openstack/oldcommon/__init__.py deleted file mode 100644 index 64da8084..00000000 --- a/windc/openstack/oldcommon/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -# TODO(jaypipes) Code in this module is intended to be ported to the eventual -# openstack-common library diff --git a/windc/openstack/oldcommon/config.py b/windc/openstack/oldcommon/config.py deleted file mode 100644 index a5fc3f09..00000000 --- a/windc/openstack/oldcommon/config.py +++ /dev/null @@ -1,337 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Routines for configuring Openstack Projects -""" - -import logging -import logging.config -import logging.handlers -import optparse -import os -import sys - -from paste import deploy - -DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s" -DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" - - -def parse_options(parser, cli_args=None): - """ - Returns the parsed CLI options, command to run and its arguments, merged - with any same-named options found in a configuration file. - - The function returns a tuple of (options, args), where options is a - mapping of option key/str(value) pairs, and args is the set of arguments - (not options) supplied on the command-line. - - The reason that the option values are returned as strings only is that - ConfigParser and paste.deploy only accept string values... - - :param parser: The option parser - :param cli_args: (Optional) Set of arguments to process. If not present, - sys.argv[1:] is used. - :retval tuple of (options, args) - """ - - (options, args) = parser.parse_args(cli_args) - - return (vars(options), args) - - -def add_common_options(parser): - """ - Given a supplied optparse.OptionParser, adds an OptionGroup that - represents all common configuration options. - - :param parser: optparse.OptionParser - """ - help_text = "The following configuration options are common to "\ - "this app's programs." - - group = optparse.OptionGroup(parser, "Common Options", help_text) - group.add_option('-v', '--verbose', default=False, dest="verbose", - action="store_true", - help="Print more verbose output") - group.add_option('-d', '--debug', default=False, dest="debug", - action="store_true", - help="Print debugging output") - group.add_option('--config-file', default=None, metavar="PATH", - help="Path to the config file to use. When not specified " - "(the default), we generally look at the first " - "argument specified to be a config file, and if " - "that is also missing, we search standard " - "directories for a config file.") - parser.add_option_group(group) - - -def add_log_options(parser): - """ - Given a supplied optparse.OptionParser, adds an OptionGroup that - represents all the configuration options around logging. - - :param parser: optparse.OptionParser - """ - help_text = "The following configuration options are specific to logging "\ - "functionality for this program." - - group = optparse.OptionGroup(parser, "Logging Options", help_text) - group.add_option('--log-config', default=None, metavar="PATH", - help="If this option is specified, the logging " - "configuration file specified is used and overrides " - "any other logging options specified. Please see " - "the Python logging module documentation for " - "details on logging configuration files.") - group.add_option('--log-date-format', metavar="FORMAT", - default=DEFAULT_LOG_DATE_FORMAT, - help="Format string for %(asctime)s in log records. " - "Default: %default") - group.add_option('--log-file', default=None, metavar="PATH", - help="(Optional) Name of log file to output to. " - "If not set, logging will go to stdout.") - group.add_option("--log-dir", default=None, - help="(Optional) The directory to keep log files in " - "(will be prepended to --logfile)") - group.add_option('--use-syslog', default=False, dest="use_syslog", - action="store_true", - help="Use syslog for logging.") - parser.add_option_group(group) - - -def setup_logging(options, conf): - """ - Sets up the logging options for a log with supplied name - - :param options: Mapping of typed option key/values - :param conf: Mapping of untyped key/values from config file - """ - - if options.get('log_config', None): - # Use a logging configuration file for all settings... - if os.path.exists(options['log_config']): - logging.config.fileConfig(options['log_config']) - return - else: - raise RuntimeError("Unable to locate specified logging " - "config file: %s" % options['log_config']) - - # If either the CLI option or the conf value - # is True, we set to True - debug = options.get('debug') or \ - get_option(conf, 'debug', type='bool', default=False) - verbose = options.get('verbose') or \ - get_option(conf, 'verbose', type='bool', default=False) - root_logger = logging.root - if debug: - root_logger.setLevel(logging.DEBUG) - elif verbose: - root_logger.setLevel(logging.INFO) - else: - root_logger.setLevel(logging.WARNING) - - # Set log configuration from options... - # Note that we use a hard-coded log format in the options - # because of Paste.Deploy bug #379 - # http://trac.pythonpaste.org/pythonpaste/ticket/379 - log_format = options.get('log_format', DEFAULT_LOG_FORMAT) - log_date_format = options.get('log_date_format', DEFAULT_LOG_DATE_FORMAT) - formatter = logging.Formatter(log_format, log_date_format) - - logfile = options.get('log_file') - if not logfile: - logfile = conf.get('log_file') - - use_syslog = options.get('use_syslog') or \ - get_option(conf, 'use_syslog', type='bool', default=False) - - if use_syslog: - handler = logging.handlers.SysLogHandler(address='/dev/log') - elif logfile: - logdir = options.get('log_dir') - if not logdir: - logdir = conf.get('log_dir') - if logdir: - logfile = os.path.join(logdir, logfile) - handler = logging.FileHandler(logfile) - else: - handler = logging.StreamHandler(sys.stdout) - - handler.setFormatter(formatter) - root_logger.addHandler(handler) - - -def fix_path(path): - """ - Return the full absolute path - """ - return os.path.abspath(os.path.expanduser(path)) - - -def find_config_file(app_name, options, args, config_dir=None): - """ - Return the first config file found for an application. - - We search for the paste config file in the following order: - * If --config-file option is used, use that - * If args[0] is a file, use that - * Search for $app.conf in standard directories: - * . - * ~.config_dir/ - * ~ - * /etc/config_dir - * /etc - - :retval Full path to config file, or None if no config file found - """ - config_dir = config_dir or app_name - - if options.get('config_file'): - if os.path.exists(options['config_file']): - return fix_path(options['config_file']) - elif args: - if os.path.exists(args[0]): - return fix_path(args[0]) - - # Handle standard directory search for $app_name.conf - config_file_dirs = [fix_path(os.getcwd()), - fix_path(os.path.join('~', '.' + config_dir)), - fix_path('~'), - os.path.join('/etc', config_dir), - '/etc'] - - for cfg_dir in config_file_dirs: - cfg_file = os.path.join(cfg_dir, '%s.conf' % app_name) - if os.path.exists(cfg_file): - return cfg_file - - -def load_paste_config(app_name, options, args, config_dir=None): - """ - Looks for a config file to use for an app and returns the - config file path and a configuration mapping from a paste config file. - - We search for the paste config file in the following order: - * If --config-file option is used, use that - * If args[0] is a file, use that - * Search for $app_name.conf in standard directories: - * . - * ~.config_dir/ - * ~ - * /etc/config_dir - * /etc - - :param app_name: Name of the application to load config for, or None. - None signifies to only load the [DEFAULT] section of - the config file. - :param options: Set of typed options returned from parse_options() - :param args: Command line arguments from argv[1:] - :retval Tuple of (conf_file, conf) - - :raises RuntimeError when config file cannot be located or there was a - problem loading the configuration file. - """ - conf_file = find_config_file(app_name, options, args, config_dir) - if not conf_file: - raise RuntimeError("Unable to locate any configuration file. " - "Cannot load application %s" % app_name) - try: - conf = deploy.appconfig("config:%s" % conf_file, name=app_name) - return conf_file, conf - except Exception, e: - raise RuntimeError("Error trying to load config %s: %s" - % (conf_file, e)) - - -def load_paste_app(app_name, options, args, config_dir=None): - """ - Builds and returns a WSGI app from a paste config file. - - We search for the paste config file in the following order: - * If --config-file option is used, use that - * If args[0] is a file, use that - * Search for $app_name.conf in standard directories: - * . - * ~.config_dir/ - * ~ - * /etc/config_dir - * /etc - - :param app_name: Name of the application to load - :param options: Set of typed options returned from parse_options() - :param args: Command line arguments from argv[1:] - - :raises RuntimeError when config file cannot be located or application - cannot be loaded from config file - """ - conf_file, conf = load_paste_config(app_name, options, - args, config_dir) - - try: - # Setup logging early, supplying both the CLI options and the - # configuration mapping from the config file - setup_logging(options, conf) - - # We only update the conf dict for the verbose and debug - # flags. Everything else must be set up in the conf file... - debug = options.get('debug') or \ - get_option(conf, 'debug', type='bool', default=False) - verbose = options.get('verbose') or \ - get_option(conf, 'verbose', type='bool', default=False) - conf['debug'] = debug - conf['verbose'] = verbose - - # Log the options used when starting if we're in debug mode... - if debug: - logger = logging.getLogger(app_name) - logger.debug("*" * 80) - logger.debug("Configuration options gathered from config file:") - logger.debug(conf_file) - logger.debug("================================================") - items = dict([(k, v) for k, v in conf.items() - if k not in ('__file__', 'here')]) - for key, value in sorted(items.items()): - logger.debug("%(key)-30s %(value)s" % locals()) - logger.debug("*" * 80) - app = deploy.loadapp("config:%s" % conf_file, name=app_name) - except (LookupError, ImportError), e: - raise RuntimeError("Unable to load %(app_name)s from " - "configuration file %(conf_file)s." - "\nGot: %(e)r" % locals()) - return conf, app - - -def get_option(options, option, **kwargs): - if option in options: - value = options[option] - type_ = kwargs.get('type', 'str') - if type_ == 'bool': - if hasattr(value, 'lower'): - return value.lower() == 'true' - else: - return value - elif type_ == 'int': - return int(value) - elif type_ == 'float': - return float(value) - else: - return value - elif 'default' in kwargs: - return kwargs['default'] - else: - raise KeyError("option '%s' not found" % option) diff --git a/windc/openstack/oldcommon/context.py b/windc/openstack/oldcommon/context.py deleted file mode 100644 index a9a16f8e..00000000 --- a/windc/openstack/oldcommon/context.py +++ /dev/null @@ -1,40 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Simple class that stores security context information in the web request. - -Projects should subclass this class if they wish to enhance the request -context or provide additional information in their specific WSGI pipeline. -""" - - -class RequestContext(object): - - """ - Stores information about the security context under which the user - accesses the system, as well as additional request information. - """ - - def __init__(self, auth_tok=None, user=None, tenant=None, is_admin=False, - read_only=False, show_deleted=False): - self.auth_tok = auth_tok - self.user = user - self.tenant = tenant - self.is_admin = is_admin - self.read_only = read_only - self.show_deleted = show_deleted diff --git a/windc/openstack/oldcommon/exception.py b/windc/openstack/oldcommon/exception.py deleted file mode 100644 index ba32da55..00000000 --- a/windc/openstack/oldcommon/exception.py +++ /dev/null @@ -1,147 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Exceptions common to OpenStack projects -""" - -import logging - - -class ProcessExecutionError(IOError): - def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None, - description=None): - if description is None: - description = "Unexpected error while running command." - if exit_code is None: - exit_code = '-' - message = "%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r" % ( - description, cmd, exit_code, stdout, stderr) - IOError.__init__(self, message) - - -class Error(Exception): - def __init__(self, message=None): - super(Error, self).__init__(message) - - -class ApiError(Error): - def __init__(self, message='Unknown', code='Unknown'): - self.message = message - self.code = code - super(ApiError, self).__init__('%s: %s' % (code, message)) - - -class NotFound(Error): - pass - - -class UnknownScheme(Error): - - msg = "Unknown scheme '%s' found in URI" - - def __init__(self, scheme): - msg = self.__class__.msg % scheme - super(UnknownScheme, self).__init__(msg) - - -class BadStoreUri(Error): - - msg = "The Store URI %s was malformed. Reason: %s" - - def __init__(self, uri, reason): - msg = self.__class__.msg % (uri, reason) - super(BadStoreUri, self).__init__(msg) - - -class Duplicate(Error): - pass - - -class NotAuthorized(Error): - pass - - -class NotEmpty(Error): - pass - - -class Invalid(Error): - pass - - -class BadInputError(Exception): - """Error resulting from a client sending bad input to a server""" - pass - - -class MissingArgumentError(Error): - pass - - -class DatabaseMigrationError(Error): - pass - - -class ClientConnectionError(Exception): - """Error resulting from a client connecting to a server""" - pass - - -def wrap_exception(f): - def _wrap(*args, **kw): - try: - return f(*args, **kw) - except Exception, e: - if not isinstance(e, Error): - #exc_type, exc_value, exc_traceback = sys.exc_info() - logging.exception('Uncaught exception') - #logging.error(traceback.extract_stack(exc_traceback)) - raise Error(str(e)) - raise - _wrap.func_name = f.func_name - return _wrap - - -class OpenstackException(Exception): - """ - Base Exception - - To correctly use this class, inherit from it and define - a 'message' property. That message will get printf'd - with the keyword arguments provided to the constructor. - """ - message = "An unknown exception occurred" - - def __init__(self, **kwargs): - try: - self._error_string = self.message % kwargs - - except Exception: - # at least get the core message out if something happened - self._error_string = self.message - - def __str__(self): - return self._error_string - - -class MalformedRequestBody(OpenstackException): - message = "Malformed message body: %(reason)s" - - -class InvalidContentType(OpenstackException): - message = "Invalid content type %(content_type)s" diff --git a/windc/openstack/oldcommon/extensions.py b/windc/openstack/oldcommon/extensions.py deleted file mode 100644 index 162a02a0..00000000 --- a/windc/openstack/oldcommon/extensions.py +++ /dev/null @@ -1,538 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# Copyright 2011 Justin Santa Barbara -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import imp -import os -import routes -import webob.dec -import webob.exc -import logging -from lxml import etree - -from openstack.common import exception -from openstack.common import wsgi - -LOG = logging.getLogger('extensions') -DEFAULT_XMLNS = "http://docs.openstack.org/" -XMLNS_ATOM = "http://www.w3.org/2005/Atom" - - -class ExtensionDescriptor(object): - """Base class that defines the contract for extensions. - - Note that you don't have to derive from this class to have a valid - extension; it is purely a convenience. - - """ - - def get_name(self): - """The name of the extension. - - e.g. 'Fox In Socks' - - """ - raise NotImplementedError() - - def get_alias(self): - """The alias for the extension. - - e.g. 'FOXNSOX' - - """ - raise NotImplementedError() - - def get_description(self): - """Friendly description for the extension. - - e.g. 'The Fox In Socks Extension' - - """ - raise NotImplementedError() - - def get_namespace(self): - """The XML namespace for the extension. - - e.g. 'http://www.fox.in.socks/api/ext/pie/v1.0' - - """ - raise NotImplementedError() - - def get_updated(self): - """The timestamp when the extension was last updated. - - e.g. '2011-01-22T13:25:27-06:00' - - """ - # NOTE(justinsb): Not sure of the purpose of this is, vs the XML NS - raise NotImplementedError() - - def get_resources(self): - """List of extensions.ResourceExtension extension objects. - - Resources define new nouns, and are accessible through URLs. - - """ - resources = [] - return resources - - def get_actions(self): - """List of extensions.ActionExtension extension objects. - - Actions are verbs callable from the API. - - """ - actions = [] - return actions - - def get_request_extensions(self): - """List of extensions.RequestException extension objects. - - Request extensions are used to handle custom request data. - - """ - request_exts = [] - return request_exts - - -class ActionExtensionController(object): - def __init__(self, application): - self.application = application - self.action_handlers = {} - - def add_action(self, action_name, handler): - self.action_handlers[action_name] = handler - - def action(self, req, id, body): - for action_name, handler in self.action_handlers.iteritems(): - if action_name in body: - return handler(body, req, id) - # no action handler found (bump to downstream application) - res = self.application - return res - - -class ActionExtensionResource(wsgi.Resource): - - def __init__(self, application): - controller = ActionExtensionController(application) - wsgi.Resource.__init__(self, controller) - - def add_action(self, action_name, handler): - self.controller.add_action(action_name, handler) - - -class RequestExtensionController(object): - - def __init__(self, application): - self.application = application - self.handlers = [] - - def add_handler(self, handler): - self.handlers.append(handler) - - def process(self, req, *args, **kwargs): - res = req.get_response(self.application) - # currently request handlers are un-ordered - for handler in self.handlers: - res = handler(req, res) - return res - - -class RequestExtensionResource(wsgi.Resource): - - def __init__(self, application): - controller = RequestExtensionController(application) - wsgi.Resource.__init__(self, controller) - - def add_handler(self, handler): - self.controller.add_handler(handler) - - -class ExtensionsResource(wsgi.Resource): - - def __init__(self, extension_manager): - self.extension_manager = extension_manager - body_serializers = {'application/xml': ExtensionsXMLSerializer()} - serializer = wsgi.ResponseSerializer(body_serializers=body_serializers) - super(ExtensionsResource, self).__init__(self, None, serializer) - - def _translate(self, ext): - ext_data = {} - ext_data['name'] = ext.get_name() - ext_data['alias'] = ext.get_alias() - ext_data['description'] = ext.get_description() - ext_data['namespace'] = ext.get_namespace() - ext_data['updated'] = ext.get_updated() - ext_data['links'] = [] # TODO(dprince): implement extension links - return ext_data - - def index(self, req): - extensions = [] - for _alias, ext in self.extension_manager.extensions.iteritems(): - extensions.append(self._translate(ext)) - return dict(extensions=extensions) - - def show(self, req, id): - # NOTE(dprince): the extensions alias is used as the 'id' for show - ext = self.extension_manager.extensions.get(id, None) - if not ext: - raise webob.exc.HTTPNotFound( - _("Extension with alias %s does not exist") % id) - - return dict(extension=self._translate(ext)) - - def delete(self, req, id): - raise webob.exc.HTTPNotFound() - - def create(self, req): - raise webob.exc.HTTPNotFound() - - -class ExtensionMiddleware(wsgi.Middleware): - """Extensions middleware for WSGI.""" - - @classmethod - def factory(cls, global_config, **local_config): - """Paste factory.""" - def _factory(app): - return cls(app, global_config, **local_config) - return _factory - - def _action_ext_resources(self, application, ext_mgr, mapper): - """Return a dict of ActionExtensionResource-s by collection.""" - action_resources = {} - for action in ext_mgr.get_actions(): - if not action.collection in action_resources.keys(): - resource = ActionExtensionResource(application) - mapper.connect("/%s/:(id)/action.:(format)" % - action.collection, - action='action', - controller=resource, - conditions=dict(method=['POST'])) - mapper.connect("/%s/:(id)/action" % - action.collection, - action='action', - controller=resource, - conditions=dict(method=['POST'])) - action_resources[action.collection] = resource - - return action_resources - - def _request_ext_resources(self, application, ext_mgr, mapper): - """Returns a dict of RequestExtensionResource-s by collection.""" - request_ext_resources = {} - for req_ext in ext_mgr.get_request_extensions(): - if not req_ext.key in request_ext_resources.keys(): - resource = RequestExtensionResource(application) - mapper.connect(req_ext.url_route + '.:(format)', - action='process', - controller=resource, - conditions=req_ext.conditions) - - mapper.connect(req_ext.url_route, - action='process', - controller=resource, - conditions=req_ext.conditions) - request_ext_resources[req_ext.key] = resource - - return request_ext_resources - - def __init__(self, application, config, ext_mgr=None): - ext_mgr = ext_mgr or ExtensionManager( - config['api_extensions_path']) - mapper = routes.Mapper() - - # extended resources - for resource_ext in ext_mgr.get_resources(): - LOG.debug(_('Extended resource: %s'), resource_ext.collection) - controller_resource = wsgi.Resource(resource_ext.controller, - resource_ext.deserializer, - resource_ext.serializer) - self._map_custom_collection_actions(resource_ext, mapper, - controller_resource) - kargs = dict(controller=controller_resource, - collection=resource_ext.collection_actions, - member=resource_ext.member_actions) - if resource_ext.parent: - kargs['parent_resource'] = resource_ext.parent - mapper.resource(resource_ext.collection, - resource_ext.collection, **kargs) - - # extended actions - action_resources = self._action_ext_resources(application, ext_mgr, - mapper) - for action in ext_mgr.get_actions(): - LOG.debug(_('Extended action: %s'), action.action_name) - resource = action_resources[action.collection] - resource.add_action(action.action_name, action.handler) - - # extended requests - req_controllers = self._request_ext_resources(application, ext_mgr, - mapper) - for request_ext in ext_mgr.get_request_extensions(): - LOG.debug(_('Extended request: %s'), request_ext.key) - controller = req_controllers[request_ext.key] - controller.add_handler(request_ext.handler) - - self._router = routes.middleware.RoutesMiddleware(self._dispatch, - mapper) - - super(ExtensionMiddleware, self).__init__(application) - - def _map_custom_collection_actions(self, resource_ext, mapper, - controller_resource): - for action, method in resource_ext.collection_actions.iteritems(): - parent = resource_ext.parent - conditions = dict(method=[method]) - path = "/%s/%s" % (resource_ext.collection, action) - - path_prefix = "" - if parent: - path_prefix = "/%s/{%s_id}" % (parent["collection_name"], - parent["member_name"]) - - with mapper.submapper(controller=controller_resource, - action=action, - path_prefix=path_prefix, - conditions=conditions) as submap: - submap.connect(path) - submap.connect("%s.:(format)" % path) - - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - """Route the incoming request with router.""" - req.environ['extended.app'] = self.application - return self._router - - @staticmethod - @webob.dec.wsgify(RequestClass=wsgi.Request) - def _dispatch(req): - """Dispatch the request. - - Returns the routed WSGI app's response or defers to the extended - application. - - """ - match = req.environ['wsgiorg.routing_args'][1] - if not match: - return req.environ['extended.app'] - app = match['controller'] - return app - - -class ExtensionManager(object): - """Load extensions from the configured extension path. - - See nova/tests/api/openstack/extensions/foxinsocks/extension.py for an - example extension implementation. - - """ - - def __init__(self, path): - LOG.debug(_('Initializing extension manager.')) - - self.path = path - self.extensions = {} - self._load_all_extensions() - - def get_resources(self): - """Returns a list of ResourceExtension objects.""" - resources = [] - extension_resource = ExtensionsResource(self) - res_ext = ResourceExtension('extensions', - extension_resource, - serializer=extension_resource.serializer) - resources.append(res_ext) - for alias, ext in self.extensions.iteritems(): - try: - resources.extend(ext.get_resources()) - except AttributeError: - # NOTE(dprince): Extension aren't required to have resource - # extensions - pass - return resources - - def get_actions(self): - """Returns a list of ActionExtension objects.""" - actions = [] - for alias, ext in self.extensions.iteritems(): - try: - actions.extend(ext.get_actions()) - except AttributeError: - # NOTE(dprince): Extension aren't required to have action - # extensions - pass - return actions - - def get_request_extensions(self): - """Returns a list of RequestExtension objects.""" - request_exts = [] - for alias, ext in self.extensions.iteritems(): - try: - request_exts.extend(ext.get_request_extensions()) - except AttributeError: - # NOTE(dprince): Extension aren't required to have request - # extensions - pass - return request_exts - - def _check_extension(self, extension): - """Checks for required methods in extension objects.""" - try: - LOG.debug(_('Ext name: %s'), extension.get_name()) - LOG.debug(_('Ext alias: %s'), extension.get_alias()) - LOG.debug(_('Ext description: %s'), extension.get_description()) - LOG.debug(_('Ext namespace: %s'), extension.get_namespace()) - LOG.debug(_('Ext updated: %s'), extension.get_updated()) - except AttributeError as ex: - LOG.exception(_("Exception loading extension: %s"), unicode(ex)) - return False - return True - - def _load_all_extensions(self): - """Load extensions from the configured path. - - Load extensions from the configured path. The extension name is - constructed from the module_name. If your extension module was named - widgets.py the extension class within that module should be - 'Widgets'. - - In addition, extensions are loaded from the 'contrib' directory. - - See nova/tests/api/openstack/extensions/foxinsocks.py for an example - extension implementation. - - """ - if os.path.exists(self.path): - self._load_all_extensions_from_path(self.path) - - contrib_path = os.path.join(os.path.dirname(__file__), "contrib") - if os.path.exists(contrib_path): - self._load_all_extensions_from_path(contrib_path) - - def _load_all_extensions_from_path(self, path): - for f in os.listdir(path): - LOG.debug(_('Loading extension file: %s'), f) - mod_name, file_ext = os.path.splitext(os.path.split(f)[-1]) - ext_path = os.path.join(path, f) - if file_ext.lower() == '.py' and not mod_name.startswith('_'): - mod = imp.load_source(mod_name, ext_path) - ext_name = mod_name[0].upper() + mod_name[1:] - new_ext_class = getattr(mod, ext_name, None) - if not new_ext_class: - LOG.warn(_('Did not find expected name ' - '"%(ext_name)s" in %(file)s'), - {'ext_name': ext_name, - 'file': ext_path}) - continue - new_ext = new_ext_class() - self.add_extension(new_ext) - - def add_extension(self, ext): - # Do nothing if the extension doesn't check out - if not self._check_extension(ext): - return - - alias = ext.get_alias() - LOG.debug(_('Loaded extension: %s'), alias) - - if alias in self.extensions: - raise exception.Error("Found duplicate extension: %s" % alias) - self.extensions[alias] = ext - - -class RequestExtension(object): - """Extend requests and responses of core nova OpenStack API resources. - - Provide a way to add data to responses and handle custom request data - that is sent to core nova OpenStack API controllers. - - """ - def __init__(self, method, url_route, handler): - self.url_route = url_route - self.handler = handler - self.conditions = dict(method=[method]) - self.key = "%s-%s" % (method, url_route) - - -class ActionExtension(object): - """Add custom actions to core nova OpenStack API resources.""" - - def __init__(self, collection, action_name, handler): - self.collection = collection - self.action_name = action_name - self.handler = handler - - -class ResourceExtension(object): - """Add top level resources to the OpenStack API in nova.""" - - def __init__(self, collection, controller, parent=None, - collection_actions=None, member_actions=None, - deserializer=None, serializer=None): - if not collection_actions: - collection_actions = {} - if not member_actions: - member_actions = {} - self.collection = collection - self.controller = controller - self.parent = parent - self.collection_actions = collection_actions - self.member_actions = member_actions - self.deserializer = deserializer - self.serializer = serializer - - -class ExtensionsXMLSerializer(wsgi.XMLDictSerializer): - - def __init__(self): - self.nsmap = {None: DEFAULT_XMLNS, 'atom': XMLNS_ATOM} - - def show(self, ext_dict): - ext = etree.Element('extension', nsmap=self.nsmap) - self._populate_ext(ext, ext_dict['extension']) - return self._to_xml(ext) - - def index(self, exts_dict): - exts = etree.Element('extensions', nsmap=self.nsmap) - for ext_dict in exts_dict['extensions']: - ext = etree.SubElement(exts, 'extension') - self._populate_ext(ext, ext_dict) - return self._to_xml(exts) - - def _populate_ext(self, ext_elem, ext_dict): - """Populate an extension xml element from a dict.""" - - ext_elem.set('name', ext_dict['name']) - ext_elem.set('namespace', ext_dict['namespace']) - ext_elem.set('alias', ext_dict['alias']) - ext_elem.set('updated', ext_dict['updated']) - desc = etree.Element('description') - desc.text = ext_dict['description'] - ext_elem.append(desc) - for link in ext_dict.get('links', []): - elem = etree.SubElement(ext_elem, '{%s}link' % XMLNS_ATOM) - elem.set('rel', link['rel']) - elem.set('href', link['href']) - elem.set('type', link['type']) - return ext_elem - - def _to_xml(self, root): - """Convert the xml object to an xml string.""" - - return etree.tostring(root, encoding='UTF-8') diff --git a/windc/openstack/oldcommon/middleware/__init__.py b/windc/openstack/oldcommon/middleware/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/windc/openstack/oldcommon/middleware/context.py b/windc/openstack/oldcommon/middleware/context.py deleted file mode 100644 index be7dafee..00000000 --- a/windc/openstack/oldcommon/middleware/context.py +++ /dev/null @@ -1,64 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Middleware that attaches a context to the WSGI request -""" - -from openstack.common import utils -from openstack.common import wsgi -from openstack.common import context - - -class ContextMiddleware(wsgi.Middleware): - def __init__(self, app, options): - self.options = options - super(ContextMiddleware, self).__init__(app) - - def make_context(self, *args, **kwargs): - """ - Create a context with the given arguments. - """ - - # Determine the context class to use - ctxcls = context.RequestContext - if 'context_class' in self.options: - ctxcls = utils.import_class(self.options['context_class']) - - return ctxcls(*args, **kwargs) - - def process_request(self, req): - """ - Extract any authentication information in the request and - construct an appropriate context from it. - """ - # Use the default empty context, with admin turned on for - # backwards compatibility - req.context = self.make_context(is_admin=True) - - -def filter_factory(global_conf, **local_conf): - """ - Factory method for paste.deploy - """ - conf = global_conf.copy() - conf.update(local_conf) - - def filter(app): - return ContextMiddleware(app, conf) - - return filter diff --git a/windc/openstack/oldcommon/utils.py b/windc/openstack/oldcommon/utils.py deleted file mode 100644 index 1faeab5a..00000000 --- a/windc/openstack/oldcommon/utils.py +++ /dev/null @@ -1,210 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -System-level utilities and helper functions. -""" - -import datetime -import logging -import os -import random -import shlex -import sys -import types - -from eventlet import greenthread -from eventlet.green import subprocess - -from openstack.common import exception - - -TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" -LOG = logging.getLogger(__name__) - - -def int_from_bool_as_string(subject): - """ - Interpret a string as a boolean and return either 1 or 0. - - Any string value in: - ('True', 'true', 'On', 'on', '1') - is interpreted as a boolean True. - - Useful for JSON-decoded stuff and config file parsing - """ - return bool_from_string(subject) and 1 or 0 - - -def bool_from_string(subject): - """ - Interpret a string as a boolean. - - Any string value in: - ('True', 'true', 'On', 'on', '1') - is interpreted as a boolean True. - - Useful for JSON-decoded stuff and config file parsing - """ - if isinstance(subject, types.BooleanType): - return subject - if isinstance(subject, types.StringTypes): - if subject.strip().lower() in ('true', 'on', '1'): - return True - return False - - -def execute(*cmd, **kwargs): - """ - Helper method to execute command with optional retry. - - :cmd Passed to subprocess.Popen. - :process_input Send to opened process. - :check_exit_code Defaults to 0. Raise exception.ProcessExecutionError - unless program exits with this code. - :delay_on_retry True | False. Defaults to True. If set to True, wait a - short amount of time before retrying. - :attempts How many times to retry cmd. - :run_as_root True | False. Defaults to False. If set to True, - the command is prefixed by the command specified - in the root_helper kwarg. - :root_helper command to prefix all cmd's with - - :raises exception.Error on receiving unknown arguments - :raises exception.ProcessExecutionError - """ - - process_input = kwargs.pop('process_input', None) - check_exit_code = kwargs.pop('check_exit_code', 0) - delay_on_retry = kwargs.pop('delay_on_retry', True) - attempts = kwargs.pop('attempts', 1) - run_as_root = kwargs.pop('run_as_root', False) - root_helper = kwargs.pop('root_helper', '') - if len(kwargs): - raise exception.Error(_('Got unknown keyword args ' - 'to utils.execute: %r') % kwargs) - if run_as_root: - cmd = shlex.split(root_helper) + list(cmd) - cmd = map(str, cmd) - - while attempts > 0: - attempts -= 1 - try: - LOG.debug(_('Running cmd (subprocess): %s'), ' '.join(cmd)) - _PIPE = subprocess.PIPE # pylint: disable=E1101 - obj = subprocess.Popen(cmd, - stdin=_PIPE, - stdout=_PIPE, - stderr=_PIPE, - close_fds=True) - result = None - if process_input is not None: - result = obj.communicate(process_input) - else: - result = obj.communicate() - obj.stdin.close() # pylint: disable=E1101 - _returncode = obj.returncode # pylint: disable=E1101 - if _returncode: - LOG.debug(_('Result was %s') % _returncode) - if type(check_exit_code) == types.IntType \ - and _returncode != check_exit_code: - (stdout, stderr) = result - raise exception.ProcessExecutionError( - exit_code=_returncode, - stdout=stdout, - stderr=stderr, - cmd=' '.join(cmd)) - return result - except exception.ProcessExecutionError: - if not attempts: - raise - else: - LOG.debug(_('%r failed. Retrying.'), cmd) - if delay_on_retry: - greenthread.sleep(random.randint(20, 200) / 100.0) - finally: - # NOTE(termie): this appears to be necessary to let the subprocess - # call clean something up in between calls, without - # it two execute calls in a row hangs the second one - greenthread.sleep(0) - - -def import_class(import_str): - """Returns a class from a string including module and class""" - mod_str, _sep, class_str = import_str.rpartition('.') - try: - __import__(mod_str) - return getattr(sys.modules[mod_str], class_str) - except (ImportError, ValueError, AttributeError): - raise exception.NotFound('Class %s cannot be found' % class_str) - - -def import_object(import_str): - """Returns an object including a module or module and class""" - try: - __import__(import_str) - return sys.modules[import_str] - except ImportError: - return import_class(import_str) - - -def isotime(at=None): - if not at: - at = datetime.datetime.utcnow() - return at.strftime(TIME_FORMAT) - - -def parse_isotime(timestr): - return datetime.datetime.strptime(timestr, TIME_FORMAT) - - -def parse_mailmap(mailmap='.mailmap'): - mapping = {} - if os.path.exists(mailmap): - fp = open(mailmap, 'r') - for l in fp: - l = l.strip() - if not l.startswith('#') and ' ' in l: - canonical_email, alias = l.split(' ') - mapping[alias] = canonical_email - return mapping - - -def str_dict_replace(s, mapping): - for s1, s2 in mapping.iteritems(): - s = s.replace(s1, s2) - return s - - -def utcnow(): - """Overridable version of utils.utcnow.""" - if utcnow.override_time: - return utcnow.override_time - return datetime.datetime.utcnow() - - -utcnow.override_time = None - - -def set_time_override(override_time=datetime.datetime.utcnow()): - """Override utils.utcnow to return a constant time.""" - utcnow.override_time = override_time - - -def clear_time_override(): - """Remove the overridden time.""" - utcnow.override_time = None diff --git a/windc/openstack/oldcommon/wsgi.py b/windc/openstack/oldcommon/wsgi.py deleted file mode 100644 index 128ae8c4..00000000 --- a/windc/openstack/oldcommon/wsgi.py +++ /dev/null @@ -1,717 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Utility methods for working with WSGI servers.""" - -import datetime -import eventlet -import eventlet.wsgi - -eventlet.patcher.monkey_patch(all=False, socket=True) - -import json -import logging -import sys -import routes -import routes.middleware -import webob.dec -import webob.exc -from xml.dom import minidom -from xml.parsers import expat - -from openstack.common import exception - - -LOG = logging.getLogger('wsgi') - - -class WritableLogger(object): - """A thin wrapper that responds to `write` and logs.""" - - def __init__(self, logger, level=logging.DEBUG): - self.logger = logger - self.level = level - - def write(self, msg): - self.logger.log(self.level, msg.strip("\n")) - - -def run_server(application, port): - """Run a WSGI server with the given application.""" - sock = eventlet.listen(('0.0.0.0', port)) - eventlet.wsgi.server(sock, application) - - -class Server(object): - """Server class to manage multiple WSGI sockets and applications.""" - - def __init__(self, threads=1000): - self.pool = eventlet.GreenPool(threads) - - def start(self, application, port, host='0.0.0.0', backlog=128): - """Run a WSGI server with the given application.""" - socket = eventlet.listen((host, port), backlog=backlog) - self.pool.spawn_n(self._run, application, socket) - - def wait(self): - """Wait until all servers have completed running.""" - try: - self.pool.waitall() - except KeyboardInterrupt: - pass - - def _run(self, application, socket): - """Start a WSGI server in a new green thread.""" - logger = logging.getLogger('eventlet.wsgi.server') - eventlet.wsgi.server(socket, application, custom_pool=self.pool, - log=WritableLogger(logger)) - - -class Middleware(object): - """ - Base WSGI middleware wrapper. These classes require an application to be - initialized that will be called next. By default the middleware will - simply call its wrapped app, or you can override __call__ to customize its - behavior. - """ - - def __init__(self, application): - self.application = application - - def process_request(self, req): - """ - Called on each request. - - If this returns None, the next application down the stack will be - executed. If it returns a response then that response will be returned - and execution will stop here. - """ - return None - - def process_response(self, response): - """Do whatever you'd like to the response.""" - return response - - @webob.dec.wsgify - def __call__(self, req): - response = self.process_request(req) - if response: - return response - response = req.get_response(self.application) - return self.process_response(response) - - -class Debug(Middleware): - """ - Helper class that can be inserted into any WSGI application chain - to get information about the request and response. - """ - - @webob.dec.wsgify - def __call__(self, req): - print ("*" * 40) + " REQUEST ENVIRON" - for key, value in req.environ.items(): - print key, "=", value - print - resp = req.get_response(self.application) - - print ("*" * 40) + " RESPONSE HEADERS" - for (key, value) in resp.headers.iteritems(): - print key, "=", value - print - - resp.app_iter = self.print_generator(resp.app_iter) - - return resp - - @staticmethod - def print_generator(app_iter): - """ - Iterator that prints the contents of a wrapper string iterator - when iterated. - """ - print ("*" * 40) + " BODY" - for part in app_iter: - sys.stdout.write(part) - sys.stdout.flush() - yield part - print - - -class Router(object): - - """ - WSGI middleware that maps incoming requests to WSGI apps. - """ - - def __init__(self, mapper): - """ - Create a router for the given routes.Mapper. - - Each route in `mapper` must specify a 'controller', which is a - WSGI app to call. You'll probably want to specify an 'action' as - well and have your controller be a wsgi.Controller, who will route - the request to the action method. - - Examples: - mapper = routes.Mapper() - sc = ServerController() - - # Explicit mapping of one route to a controller+action - mapper.connect(None, "/svrlist", controller=sc, action="list") - - # Actions are all implicitly defined - mapper.resource("server", "servers", controller=sc) - - # Pointing to an arbitrary WSGI app. You can specify the - # {path_info:.*} parameter so the target app can be handed just that - # section of the URL. - mapper.connect(None, "/v1.0/{path_info:.*}", controller=BlogApp()) - """ - self.map = mapper - self._router = routes.middleware.RoutesMiddleware(self._dispatch, - self.map) - - @webob.dec.wsgify - def __call__(self, req): - """ - Route the incoming request to a controller based on self.map. - If no match, return a 404. - """ - return self._router - - @staticmethod - @webob.dec.wsgify - def _dispatch(req): - """ - Called by self._router after matching the incoming request to a route - and putting the information into req.environ. Either returns 404 - or the routed WSGI app's response. - """ - match = req.environ['wsgiorg.routing_args'][1] - if not match: - return webob.exc.HTTPNotFound() - app = match['controller'] - return app - - -class Request(webob.Request): - """Add some Openstack API-specific logic to the base webob.Request.""" - - default_request_content_types = ('application/json', 'application/xml') - default_accept_types = ('application/json', 'application/xml') - default_accept_type = 'application/json' - - def best_match_content_type(self, supported_content_types=None): - """Determine the requested response content-type. - - Based on the query extension then the Accept header. - Defaults to default_accept_type if we don't find a preference - - """ - supported_content_types = (supported_content_types or - self.default_accept_types) - - parts = self.path.rsplit('.', 1) - if len(parts) > 1: - ctype = 'application/{0}'.format(parts[1]) - if ctype in supported_content_types: - return ctype - - bm = self.accept.best_match(supported_content_types) - return bm or self.default_accept_type - - def get_content_type(self, allowed_content_types=None): - """Determine content type of the request body. - - Does not do any body introspection, only checks header - - """ - if not "Content-Type" in self.headers: - return None - - content_type = self.content_type - allowed_content_types = (allowed_content_types or - self.default_request_content_types) - - if content_type not in allowed_content_types: - raise exception.InvalidContentType(content_type=content_type) - return content_type - - -class Resource(object): - """ - WSGI app that handles (de)serialization and controller dispatch. - - Reads routing information supplied by RoutesMiddleware and calls - the requested action method upon its deserializer, controller, - and serializer. Those three objects may implement any of the basic - controller action methods (create, update, show, index, delete) - along with any that may be specified in the api router. A 'default' - method may also be implemented to be used in place of any - non-implemented actions. Deserializer methods must accept a request - argument and return a dictionary. Controller methods must accept a - request argument. Additionally, they must also accept keyword - arguments that represent the keys returned by the Deserializer. They - may raise a webob.exc exception or return a dict, which will be - serialized by requested content type. - """ - def __init__(self, controller, deserializer=None, serializer=None): - """ - :param controller: object that implement methods created by routes lib - :param deserializer: object that supports webob request deserialization - through controller-like actions - :param serializer: object that supports webob response serialization - through controller-like actions - """ - self.controller = controller - self.serializer = serializer or ResponseSerializer() - self.deserializer = deserializer or RequestDeserializer() - - @webob.dec.wsgify(RequestClass=Request) - def __call__(self, request): - """WSGI method that controls (de)serialization and method dispatch.""" - - try: - action, action_args, accept = self.deserialize_request(request) - except exception.InvalidContentType: - msg = _("Unsupported Content-Type") - return webob.exc.HTTPUnsupportedMediaType(explanation=msg) - except exception.MalformedRequestBody: - msg = _("Malformed request body") - return webob.exc.HTTPBadRequest(explanation=msg) - - action_result = self.execute_action(action, request, **action_args) - try: - return self.serialize_response(action, action_result, accept) - # return unserializable result (typically a webob exc) - except Exception: - return action_result - - def deserialize_request(self, request): - return self.deserializer.deserialize(request) - - def serialize_response(self, action, action_result, accept): - return self.serializer.serialize(action_result, accept, action) - - def execute_action(self, action, request, **action_args): - return self.dispatch(self.controller, action, request, **action_args) - - def dispatch(self, obj, action, *args, **kwargs): - """Find action-specific method on self and call it.""" - try: - method = getattr(obj, action) - except AttributeError: - method = getattr(obj, 'default') - - return method(*args, **kwargs) - - def get_action_args(self, request_environment): - """Parse dictionary created by routes library.""" - try: - args = request_environment['wsgiorg.routing_args'][1].copy() - except Exception: - return {} - - try: - del args['controller'] - except KeyError: - pass - - try: - del args['format'] - except KeyError: - pass - - return args - - -class ActionDispatcher(object): - """Maps method name to local methods through action name.""" - - def dispatch(self, *args, **kwargs): - """Find and call local method.""" - action = kwargs.pop('action', 'default') - action_method = getattr(self, str(action), self.default) - return action_method(*args, **kwargs) - - def default(self, data): - raise NotImplementedError() - - -class DictSerializer(ActionDispatcher): - """Default request body serialization""" - - def serialize(self, data, action='default'): - return self.dispatch(data, action=action) - - def default(self, data): - return "" - - -class JSONDictSerializer(DictSerializer): - """Default JSON request body serialization""" - - def default(self, data): - def sanitizer(obj): - if isinstance(obj, datetime.datetime): - _dtime = obj - datetime.timedelta(microseconds=obj.microsecond) - return _dtime.isoformat() - return obj - return json.dumps(data, default=sanitizer) - - -class XMLDictSerializer(DictSerializer): - - def __init__(self, metadata=None, xmlns=None): - """ - :param metadata: information needed to deserialize xml into - a dictionary. - :param xmlns: XML namespace to include with serialized xml - """ - super(XMLDictSerializer, self).__init__() - self.metadata = metadata or {} - self.xmlns = xmlns - - def default(self, data): - # We expect data to contain a single key which is the XML root. - root_key = data.keys()[0] - doc = minidom.Document() - node = self._to_xml_node(doc, self.metadata, root_key, data[root_key]) - - return self.to_xml_string(node) - - def to_xml_string(self, node, has_atom=False): - self._add_xmlns(node, has_atom) - return node.toprettyxml(indent=' ', encoding='UTF-8') - - #NOTE (ameade): the has_atom should be removed after all of the - # xml serializers and view builders have been updated to the current - # spec that required all responses include the xmlns:atom, the has_atom - # flag is to prevent current tests from breaking - def _add_xmlns(self, node, has_atom=False): - if self.xmlns is not None: - node.setAttribute('xmlns', self.xmlns) - if has_atom: - node.setAttribute('xmlns:atom', "http://www.w3.org/2005/Atom") - - def _to_xml_node(self, doc, metadata, nodename, data): - """Recursive method to convert data members to XML nodes.""" - result = doc.createElement(nodename) - - # Set the xml namespace if one is specified - # TODO(justinsb): We could also use prefixes on the keys - xmlns = metadata.get('xmlns', None) - if xmlns: - result.setAttribute('xmlns', xmlns) - - #TODO(bcwaldon): accomplish this without a type-check - if type(data) is list: - collections = metadata.get('list_collections', {}) - if nodename in collections: - metadata = collections[nodename] - for item in data: - node = doc.createElement(metadata['item_name']) - node.setAttribute(metadata['item_key'], str(item)) - result.appendChild(node) - return result - singular = metadata.get('plurals', {}).get(nodename, None) - if singular is None: - if nodename.endswith('s'): - singular = nodename[:-1] - else: - singular = 'item' - for item in data: - node = self._to_xml_node(doc, metadata, singular, item) - result.appendChild(node) - #TODO(bcwaldon): accomplish this without a type-check - elif type(data) is dict: - collections = metadata.get('dict_collections', {}) - if nodename in collections: - metadata = collections[nodename] - for k, v in data.items(): - node = doc.createElement(metadata['item_name']) - node.setAttribute(metadata['item_key'], str(k)) - text = doc.createTextNode(str(v)) - node.appendChild(text) - result.appendChild(node) - return result - attrs = metadata.get('attributes', {}).get(nodename, {}) - for k, v in data.items(): - if k in attrs: - result.setAttribute(k, str(v)) - else: - node = self._to_xml_node(doc, metadata, k, v) - result.appendChild(node) - else: - # Type is atom - node = doc.createTextNode(str(data)) - result.appendChild(node) - return result - - def _create_link_nodes(self, xml_doc, links): - link_nodes = [] - for link in links: - link_node = xml_doc.createElement('atom:link') - link_node.setAttribute('rel', link['rel']) - link_node.setAttribute('href', link['href']) - if 'type' in link: - link_node.setAttribute('type', link['type']) - link_nodes.append(link_node) - return link_nodes - - -class ResponseHeadersSerializer(ActionDispatcher): - """Default response headers serialization""" - - def serialize(self, response, data, action): - self.dispatch(response, data, action=action) - - def default(self, response, data): - response.status_int = 200 - - -class ResponseSerializer(object): - """Encode the necessary pieces into a response object""" - - def __init__(self, body_serializers=None, headers_serializer=None): - self.body_serializers = { - 'application/xml': XMLDictSerializer(), - 'application/json': JSONDictSerializer(), - } - self.body_serializers.update(body_serializers or {}) - - self.headers_serializer = headers_serializer or \ - ResponseHeadersSerializer() - - def serialize(self, response_data, content_type, action='default'): - """Serialize a dict into a string and wrap in a wsgi.Request object. - - :param response_data: dict produced by the Controller - :param content_type: expected mimetype of serialized response body - - """ - response = webob.Response() - self.serialize_headers(response, response_data, action) - self.serialize_body(response, response_data, content_type, action) - return response - - def serialize_headers(self, response, data, action): - self.headers_serializer.serialize(response, data, action) - - def serialize_body(self, response, data, content_type, action): - response.headers['Content-Type'] = content_type - if data is not None: - serializer = self.get_body_serializer(content_type) - response.body = serializer.serialize(data, action) - - def get_body_serializer(self, content_type): - try: - return self.body_serializers[content_type] - except (KeyError, TypeError): - raise exception.InvalidContentType(content_type=content_type) - - -class RequestHeadersDeserializer(ActionDispatcher): - """Default request headers deserializer""" - - def deserialize(self, request, action): - return self.dispatch(request, action=action) - - def default(self, request): - return {} - - -class RequestDeserializer(object): - """Break up a Request object into more useful pieces.""" - - def __init__(self, body_deserializers=None, headers_deserializer=None, - supported_content_types=None): - - self.supported_content_types = supported_content_types - - self.body_deserializers = { - 'application/xml': XMLDeserializer(), - 'application/json': JSONDeserializer(), - } - self.body_deserializers.update(body_deserializers or {}) - - self.headers_deserializer = headers_deserializer or \ - RequestHeadersDeserializer() - - def deserialize(self, request): - """Extract necessary pieces of the request. - - :param request: Request object - :returns tuple of expected controller action name, dictionary of - keyword arguments to pass to the controller, the expected - content type of the response - - """ - action_args = self.get_action_args(request.environ) - action = action_args.pop('action', None) - - action_args.update(self.deserialize_headers(request, action)) - action_args.update(self.deserialize_body(request, action)) - - accept = self.get_expected_content_type(request) - - return (action, action_args, accept) - - def deserialize_headers(self, request, action): - return self.headers_deserializer.deserialize(request, action) - - def deserialize_body(self, request, action): - if not len(request.body) > 0: - LOG.debug(_("Empty body provided in request")) - return {} - - try: - content_type = request.get_content_type() - except exception.InvalidContentType: - LOG.debug(_("Unrecognized Content-Type provided in request")) - raise - - if content_type is None: - LOG.debug(_("No Content-Type provided in request")) - return {} - - try: - deserializer = self.get_body_deserializer(content_type) - except exception.InvalidContentType: - LOG.debug(_("Unable to deserialize body as provided Content-Type")) - raise - - return deserializer.deserialize(request.body, action) - - def get_body_deserializer(self, content_type): - try: - return self.body_deserializers[content_type] - except (KeyError, TypeError): - raise exception.InvalidContentType(content_type=content_type) - - def get_expected_content_type(self, request): - return request.best_match_content_type(self.supported_content_types) - - def get_action_args(self, request_environment): - """Parse dictionary created by routes library.""" - try: - args = request_environment['wsgiorg.routing_args'][1].copy() - except Exception: - return {} - - try: - del args['controller'] - except KeyError: - pass - - try: - del args['format'] - except KeyError: - pass - - return args - - -class TextDeserializer(ActionDispatcher): - """Default request body deserialization""" - - def deserialize(self, datastring, action='default'): - return self.dispatch(datastring, action=action) - - def default(self, datastring): - return {} - - -class JSONDeserializer(TextDeserializer): - - def _from_json(self, datastring): - try: - return json.loads(datastring) - except ValueError: - msg = _("cannot understand JSON") - raise exception.MalformedRequestBody(reason=msg) - - def default(self, datastring): - return {'body': self._from_json(datastring)} - - -class XMLDeserializer(TextDeserializer): - - def __init__(self, metadata=None): - """ - :param metadata: information needed to deserialize xml into - a dictionary. - """ - super(XMLDeserializer, self).__init__() - self.metadata = metadata or {} - - def _from_xml(self, datastring): - plurals = set(self.metadata.get('plurals', {})) - - try: - node = minidom.parseString(datastring).childNodes[0] - return {node.nodeName: self._from_xml_node(node, plurals)} - except expat.ExpatError: - msg = _("cannot understand XML") - raise exception.MalformedRequestBody(reason=msg) - - def _from_xml_node(self, node, listnames): - """Convert a minidom node to a simple Python type. - - :param listnames: list of XML node names whose subnodes should - be considered list items. - - """ - - if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3: - return node.childNodes[0].nodeValue - elif node.nodeName in listnames: - return [self._from_xml_node(n, listnames) for n in node.childNodes] - else: - result = dict() - for attr in node.attributes.keys(): - result[attr] = node.attributes[attr].nodeValue - for child in node.childNodes: - if child.nodeType != node.TEXT_NODE: - result[child.nodeName] = self._from_xml_node(child, - listnames) - return result - - def find_first_child_named(self, parent, name): - """Search a nodes children for the first child with a given name""" - for node in parent.childNodes: - if node.nodeName == name: - return node - return None - - def find_children_named(self, parent, name): - """Return all of a nodes children who have the given name""" - for node in parent.childNodes: - if node.nodeName == name: - yield node - - def extract_text(self, node): - """Get the text field contained by the given node""" - if len(node.childNodes) == 1: - child = node.childNodes[0] - if child.nodeType == child.TEXT_NODE: - return child.nodeValue - return "" - - def default(self, datastring): - return {'body': self._from_xml(datastring)} diff --git a/windc/run_tests.log b/windc/run_tests.log deleted file mode 100644 index dc224f3f..00000000 --- a/windc/run_tests.log +++ /dev/null @@ -1,3 +0,0 @@ -nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] -nose.selector: INFO: /Users/gokrokve/Keero/windc/run_tests.sh is executable; skipped -2013-02-11 13:35:01,989 DEBUG Initialized with method overriding = True, and path info altering = True diff --git a/windc/run_tests.py b/windc/run_tests.py deleted file mode 100644 index 8f59fe25..00000000 --- a/windc/run_tests.py +++ /dev/null @@ -1,293 +0,0 @@ -#!/usr/bin/env python -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack, LLC -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Colorizer Code is borrowed from Twisted: -# Copyright (c) 2001-2010 Twisted Matrix Laboratories. -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -""" -Unittest runner for balancer - -To run all test:: - python run_tests.py - -To run a single test:: - python run_tests.py test_stores:TestSwiftBackend.test_get - -To run a single test module:: - python run_tests.py test_stores -""" - -import gettext -import logging -import os -import unittest -import sys - -gettext.install('windc', unicode=1) - -from nose import config -from nose import result -from nose import core - - -class _AnsiColorizer(object): - """ - A colorizer is an object that loosely wraps around a stream, allowing - callers to write text to the stream in a particular color. - - Colorizer classes must implement C{supported()} and C{write(text, color)}. - """ - _colors = dict(black=30, red=31, green=32, yellow=33, - blue=34, magenta=35, cyan=36, white=37) - - def __init__(self, stream): - self.stream = stream - - def supported(cls, stream=sys.stdout): - """ - A class method that returns True if the current platform supports - coloring terminal output using this method. Returns False otherwise. - """ - if not stream.isatty(): - return False # auto color only on TTYs - try: - import curses - except ImportError: - return False - else: - try: - try: - return curses.tigetnum("colors") > 2 - except curses.error: - curses.setupterm() - return curses.tigetnum("colors") > 2 - except: - raise - # guess false in case of error - return False - supported = classmethod(supported) - - def write(self, text, color): - """ - Write the given text to the stream in the given color. - - @param text: Text to be written to the stream. - - @param color: A string label for a color. e.g. 'red', 'white'. - """ - color = self._colors[color] - self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text)) - - -class _Win32Colorizer(object): - """ - See _AnsiColorizer docstring. - """ - def __init__(self, stream): - from win32console import GetStdHandle, STD_OUT_HANDLE, \ - FOREGROUND_RED, FOREGROUND_BLUE, FOREGROUND_GREEN, \ - FOREGROUND_INTENSITY - red, green, blue, bold = (FOREGROUND_RED, FOREGROUND_GREEN, - FOREGROUND_BLUE, FOREGROUND_INTENSITY) - self.stream = stream - self.screenBuffer = GetStdHandle(STD_OUT_HANDLE) - self._colors = { - 'normal': red | green | blue, - 'red': red | bold, - 'green': green | bold, - 'blue': blue | bold, - 'yellow': red | green | bold, - 'magenta': red | blue | bold, - 'cyan': green | blue | bold, - 'white': red | green | blue | bold} - - def supported(cls, stream=sys.stdout): - try: - import win32console - screenBuffer = win32console.GetStdHandle( - win32console.STD_OUT_HANDLE) - except ImportError: - return False - import pywintypes - try: - screenBuffer.SetConsoleTextAttribute( - win32console.FOREGROUND_RED | - win32console.FOREGROUND_GREEN | - win32console.FOREGROUND_BLUE) - except pywintypes.error: - return False - else: - return True - supported = classmethod(supported) - - def write(self, text, color): - color = self._colors[color] - self.screenBuffer.SetConsoleTextAttribute(color) - self.stream.write(text) - self.screenBuffer.SetConsoleTextAttribute(self._colors['normal']) - - -class _NullColorizer(object): - """ - See _AnsiColorizer docstring. - """ - def __init__(self, stream): - self.stream = stream - - def supported(cls, stream=sys.stdout): - return True - supported = classmethod(supported) - - def write(self, text, color): - self.stream.write(text) - - -class WindcTestResult(result.TextTestResult): - def __init__(self, *args, **kw): - result.TextTestResult.__init__(self, *args, **kw) - self._last_case = None - self.colorizer = None - # NOTE(vish, tfukushima): reset stdout for the terminal check - stdout = sys.stdout - sys.stdout = sys.__stdout__ - for colorizer in [_Win32Colorizer, _AnsiColorizer, _NullColorizer]: - if colorizer.supported(): - self.colorizer = colorizer(self.stream) - break - sys.stdout = stdout - - def getDescription(self, test): - return str(test) - - # NOTE(vish, tfukushima): copied from unittest with edit to add color - def addSuccess(self, test): - unittest.TestResult.addSuccess(self, test) - if self.showAll: - self.colorizer.write("OK", 'green') - self.stream.writeln() - elif self.dots: - self.stream.write('.') - self.stream.flush() - - # NOTE(vish, tfukushima): copied from unittest with edit to add color - def addFailure(self, test, err): - unittest.TestResult.addFailure(self, test, err) - if self.showAll: - self.colorizer.write("FAIL", 'red') - self.stream.writeln() - elif self.dots: - self.stream.write('F') - self.stream.flush() - - # NOTE(vish, tfukushima): copied from unittest with edit to add color - def addError(self, test, err): - """ - Overrides normal addError to add support for errorClasses. - If the exception is a registered class, the error will be added - to the list for that class, not errors. - """ - stream = getattr(self, 'stream', None) - ec, ev, tb = err - try: - exc_info = self._exc_info_to_string(err, test) - except TypeError: - # This is for compatibility with Python 2.3. - exc_info = self._exc_info_to_string(err) - for cls, (storage, label, isfail) in self.errorClasses.items(): - if result.isclass(ec) and issubclass(ec, cls): - if isfail: - test.passwd = False - storage.append((test, exc_info)) - # Might get patched into a streamless result - if stream is not None: - if self.showAll: - message = [label] - detail = result._exception_detail(err[1]) - if detail: - message.append(detail) - stream.writeln(": ".join(message)) - elif self.dots: - stream.write(label[:1]) - return - self.errors.append((test, exc_info)) - test.passed = False - if stream is not None: - if self.showAll: - self.colorizer.write("ERROR", 'red') - self.stream.writeln() - elif self.dots: - stream.write('E') - - def startTest(self, test): - unittest.TestResult.startTest(self, test) - current_case = test.test.__class__.__name__ - - if self.showAll: - if current_case != self._last_case: - self.stream.writeln(current_case) - self._last_case = current_case - - self.stream.write( - ' %s' % str(test.test._testMethodName).ljust(60)) - self.stream.flush() - - -class WindcTestRunner(core.TextTestRunner): - def _makeResult(self): - return WindcTestResult(self.stream, - self.descriptions, - self.verbosity, - self.config) - - -if __name__ == '__main__': - logger = logging.getLogger() - hdlr = logging.StreamHandler() - formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') - hdlr.setFormatter(formatter) - logger.addHandler(hdlr) - logger.setLevel(logging.INFO) - - c = config.Config(stream=sys.stdout, - env=os.environ, - verbosity=3, - plugins=core.DefaultPluginManager()) - - runner = WindcTestRunner(stream=c.stream, - verbosity=c.verbosity, - config=c) - sys.exit(not core.run(config=c, testRunner=runner)) diff --git a/windc/run_tests.sh b/windc/run_tests.sh deleted file mode 100755 index 2fb93e1d..00000000 --- a/windc/run_tests.sh +++ /dev/null @@ -1,129 +0,0 @@ -#!/bin/bash - -function usage { - echo "Usage: $0 [OPTION]..." - echo "Run Loadbalancer's test suite(s)" - echo "" - echo " -V, --virtual-env Always use virtualenv. Install automatically if not present" - echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment" - echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added." - echo " --unittests-only Run unit tests only, exclude functional tests." - echo " -c, --coverage Generate coverage report" - echo " -p, --pep8 Just run pep8" - echo " -h, --help Print this usage message" - echo "" - echo "Note: with no options specified, the script will try to run the tests in a virtual environment," - echo " If no virtualenv is found, the script will ask if you would like to create one. If you " - echo " prefer to run tests NOT in a virtual environment, simply pass the -N option." - exit -} - -function process_option { - case "$1" in - -h|--help) usage;; - -V|--virtual-env) let always_venv=1; let never_venv=0;; - -N|--no-virtual-env) let always_venv=0; let never_venv=1;; - -p|--pep8) let just_pep8=1;; - -f|--force) let force=1;; - --unittests-only) noseopts="$noseopts --exclude-dir=windc/tests/functional";; - -c|--coverage) coverage=1;; - -*) noseopts="$noseopts $1";; - *) noseargs="$noseargs $1" - esac -} - -venv=.venv -with_venv=tools/with_venv.sh -always_venv=0 -never_venv=0 -force=0 -noseargs= -noseopts= -wrapper="" -just_pep8=0 -coverage=0 - -for arg in "$@"; do - process_option $arg -done - -# If enabled, tell nose to collect coverage data -if [ $coverage -eq 1 ]; then - noseopts="$noseopts --with-coverage --cover-package=windc --cover-inclusive" -fi - -function run_tests { - # Just run the test suites in current environment - ${wrapper} $NOSETESTS 2> run_tests.log -} - -function run_pep8 { - echo "Running pep8 ..." - PEP8_OPTIONS="--exclude=$PEP8_EXCLUDE --repeat" - PEP8_INCLUDE="bin/* windc tools setup.py run_tests.py" - ${wrapper} pep8 $PEP8_OPTIONS $PEP8_INCLUDE - PEP_RESULT=$? - case "$TERM" in - *color* ) function out { printf "\033[3%d;1m%s\033[m\n" "$1" "$2"; } ;; - * ) function out { printf "%s\n" "$2"; } ;; - esac - if [ $PEP_RESULT -eq 0 ]; then - out 2 "PEP8 OK" - else - out 1 "PEP8 FAIL" - fi - return $PEP_RESULT -} - - -NOSETESTS="python run_tests.py $noseopts $noseargs" - -if [ $never_venv -eq 0 ] -then - # Remove the virtual environment if --force used - if [ $force -eq 1 ]; then - echo "Cleaning virtualenv..." - rm -rf ${venv} - fi - if [ -e ${venv} ]; then - wrapper="${with_venv}" - else - if [ $always_venv -eq 1 ]; then - # Automatically install the virtualenv - python tools/install_venv.py || exit 1 - wrapper="${with_venv}" - else - echo -e "No virtual environment found...create one? (Y/n) \c" - read use_ve - if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then - # Install the virtualenv and run the test suite in it - python tools/install_venv.py || exit 1 - wrapper=${with_venv} - fi - fi - fi -fi - -# Delete old coverage data from previous runs -if [ $coverage -eq 1 ]; then - ${wrapper} coverage erase -fi - -if [ $just_pep8 -eq 1 ]; then - run_pep8 - exit $? -fi - -run_tests -TEST_RESULT=$? - -if [ -z "$noseargs" ]; then - run_pep8 || exit 1 -fi - -if [ $coverage -eq 1 ]; then - echo "Generating coverage report in covhtml/" - ${wrapper} coverage html -d covhtml -i --include='windc/*' --omit='windc/db/migrate_repo*,windc/common*,windc/tests*' -fi - -exit $TEST_RESULT diff --git a/windc/setup.cfg b/windc/setup.cfg deleted file mode 100644 index d53addcb..00000000 --- a/windc/setup.cfg +++ /dev/null @@ -1,9 +0,0 @@ -[build_sphinx] -all_files = 1 -build-dir = doc/build -source-dir = doc/source - -[egg_info] -tag_build = -tag_date = 0 -tag_svn_revision = 0 diff --git a/windc/setup.py b/windc/setup.py deleted file mode 100644 index 3265bcab..00000000 --- a/windc/setup.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/python -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import os -import subprocess - -from setuptools import setup, find_packages -from setuptools.command.sdist import sdist - -from windc import version - - -if os.path.isdir('.bzr'): - with open("windc/vcsversion.py", 'w') as version_file: - vcs_cmd = subprocess.Popen(["bzr", "version-info", "--python"], - stdout=subprocess.PIPE) - vcsversion = vcs_cmd.communicate()[0] - version_file.write(vcsversion) - - -class local_sdist(sdist): - """Customized sdist hook - builds the ChangeLog file from VC first""" - - def run(self): - if os.path.isdir('.bzr'): - # We're in a bzr branch - - log_cmd = subprocess.Popen(["bzr", "log", "--gnu"], - stdout=subprocess.PIPE) - changelog = log_cmd.communicate()[0] - with open("ChangeLog", "w") as changelog_file: - changelog_file.write(changelog) - sdist.run(self) - -cmdclass = {'sdist': local_sdist} - -# If Sphinx is installed on the box running setup.py, -# enable setup.py to build the documentation, otherwise, -# just ignore it -try: - from sphinx.setup_command import BuildDoc - - class local_BuildDoc(BuildDoc): - def run(self): - for builder in ['html', 'man']: - self.builder = builder - self.finalize_options() - BuildDoc.run(self) - cmdclass['build_sphinx'] = local_BuildDoc - -except: - pass - - -setup( - name='windc', - version=version.canonical_version_string(), - description='The WinDC project provides a simple WSGI server for Windows Environment Management', - license='Apache License (2.0)', - author='OpenStack', - author_email='openstack@lists.launchpad.net', - url='http://windc.openstack.org/', - packages=find_packages(exclude=['tests', 'bin']), - test_suite='nose.collector', - cmdclass=cmdclass, - include_package_data=True, - classifiers=[ - 'Development Status :: 4 - Beta', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python :: 2.6', - 'Environment :: No Input/Output (Daemon)', - ], - scripts=['bin/windc', - 'bin/windc-api']) diff --git a/windc/tests/__init__.py b/windc/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/windc/tests/functional/__init__.py b/windc/tests/functional/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/windc/tests/manual/createDataCenter.sh b/windc/tests/manual/createDataCenter.sh deleted file mode 100755 index 2c1e5315..00000000 --- a/windc/tests/manual/createDataCenter.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -URL=http://localhost:8082/foo/datacenters -curl -v -H "Content-Type: application/json" -X POST -d@createDataCenterParameters$1 $URL diff --git a/windc/tests/manual/createDataCenterParameters b/windc/tests/manual/createDataCenterParameters deleted file mode 100644 index 6200231d..00000000 --- a/windc/tests/manual/createDataCenterParameters +++ /dev/null @@ -1,7 +0,0 @@ -{ -"name": "Test Data Center 2", -"type": "SingleZone", -"version":"1.1", -"KMS":"172.16.1.2", -"WSUS":"172.16.1.3" -} diff --git a/windc/tests/manual/createService.sh b/windc/tests/manual/createService.sh deleted file mode 100755 index 7a508884..00000000 --- a/windc/tests/manual/createService.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -URL=http://localhost:8082/foo/datacenters/$1/services -curl -v -H "Content-Type: application/json" -X POST -d@createServiceParameters$2 $URL diff --git a/windc/tests/manual/createServiceParameters b/windc/tests/manual/createServiceParameters deleted file mode 100644 index 0954f925..00000000 --- a/windc/tests/manual/createServiceParameters +++ /dev/null @@ -1,8 +0,0 @@ -{ -"type": "active_directory_service", -"zones": ["zone1"], -"domain": "ACME.cloud", -"AdminUser": "Admin", -"AdminPassword": "StrongPassword", -"DomainControllerNames": ["AD-DC001"] -} diff --git a/windc/tests/manual/listDataCenter.sh b/windc/tests/manual/listDataCenter.sh deleted file mode 100755 index bb956800..00000000 --- a/windc/tests/manual/listDataCenter.sh +++ /dev/null @@ -1 +0,0 @@ -curl -X GET http://localhost:8082/foo/datacenters diff --git a/windc/tests/unit/__init__.py b/windc/tests/unit/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/windc/tests/unit/test_base_driver.py b/windc/tests/unit/test_base_driver.py deleted file mode 100644 index dafdf733..00000000 --- a/windc/tests/unit/test_base_driver.py +++ /dev/null @@ -1,14 +0,0 @@ -import unittest -import mock - - -from windc.api.v1.router import API - - -class TestBaseDriver(unittest.TestCase): - def setUp(self): - super(TestBaseDriver, self).setUp() - self.conf = mock.Mock() - - def testAPI(self): - api = API(None) diff --git a/windc/tools/install_venv.py b/windc/tools/install_venv.py deleted file mode 100644 index c3b81718..00000000 --- a/windc/tools/install_venv.py +++ /dev/null @@ -1,154 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2010 OpenStack LLC. -# -# 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. - -""" -Installation script for Glance's development virtualenv -""" - -import os -import subprocess -import sys - - -ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) -VENV = os.path.join(ROOT, '.venv') -PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires') -TEST_REQUIRES = os.path.join(ROOT, 'tools', 'test-requires') - - -def die(message, *args): - print >> sys.stderr, message % args - sys.exit(1) - - -def run_command(cmd, redirect_output=True, check_exit_code=True): - """ - Runs a command in an out-of-process shell, returning the - output of that command. Working directory is ROOT. - """ - if redirect_output: - stdout = subprocess.PIPE - else: - stdout = None - - proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout) - output = proc.communicate()[0] - if check_exit_code and proc.returncode != 0: - die('Command "%s" failed.\n%s', ' '.join(cmd), output) - return output - - -HAS_EASY_INSTALL = bool(run_command(['which', 'easy_install'], - check_exit_code=False).strip()) -HAS_VIRTUALENV = bool(run_command(['which', 'virtualenv'], - check_exit_code=False).strip()) - - -def check_dependencies(): - """Make sure virtualenv is in the path.""" - - if not HAS_VIRTUALENV: - print 'not found.' - # Try installing it via easy_install... - if HAS_EASY_INSTALL: - print 'Installing virtualenv via easy_install...', - if not run_command(['which', 'easy_install']): - die('ERROR: virtualenv not found.\n\n' - 'Balancer development requires virtualenv, please install' - ' it using your favorite package management tool') - print 'done.' - print 'done.' - - -def create_virtualenv(venv=VENV): - """ - Creates the virtual environment and installs PIP only into the - virtual environment - """ - print 'Creating venv...', - run_command(['virtualenv', '-q', '--no-site-packages', VENV]) - print 'done.' - print 'Installing pip in virtualenv...', - if not run_command(['tools/with_venv.sh', 'easy_install', - 'pip>1.0']).strip(): - die("Failed to install pip.") - print 'done.' - - -def pip_install(*args): - run_command(['tools/with_venv.sh', - 'pip', 'install', '--upgrade'] + list(args), - redirect_output=False) - - -def install_dependencies(venv=VENV): - print 'Installing dependencies with pip (this can take a while)...' - - pip_install('pip') - - pip_install('-r', PIP_REQUIRES) - pip_install('-r', TEST_REQUIRES) - - # Tell the virtual env how to "import glance" - py_ver = _detect_python_version(venv) - pthfile = os.path.join(venv, "lib", py_ver, - "site-packages", "balancer.pth") - f = open(pthfile, 'w') - f.write("%s\n" % ROOT) - - -def _detect_python_version(venv): - lib_dir = os.path.join(venv, "lib") - for pathname in os.listdir(lib_dir): - if pathname.startswith('python'): - return pathname - raise Exception('Unable to detect Python version') - - -def print_help(): - help = """ - Glance development environment setup is complete. - - Glance development uses virtualenv to track and manage Python dependencies - while in development and testing. - - To activate the Glance virtualenv for the extent of your current shell session - you can run: - - $ source .venv/bin/activate - - Or, if you prefer, you can run commands in the virtualenv on a case by case - basis by running: - - $ tools/with_venv.sh - - Also, make test will automatically use the virtualenv. - """ - print help - - -def main(argv): - check_dependencies() - create_virtualenv() - install_dependencies() - print_help() - -if __name__ == '__main__': - main(sys.argv) diff --git a/windc/tools/pip-requires b/windc/tools/pip-requires deleted file mode 100644 index 20109d9f..00000000 --- a/windc/tools/pip-requires +++ /dev/null @@ -1,23 +0,0 @@ -# The greenlet package must be compiled with gcc and needs -# the Python.h headers. Make sure you install the python-dev -# package to get the right headers... -greenlet>=0.3.1 - -SQLAlchemy<=0.7.9 -anyjson -eventlet>=0.9.12 -PasteDeploy -Routes -webob==1.0.8 -wsgiref -argparse -sqlalchemy-migrate>=0.7.2 -httplib2 -kombu -iso8601>=0.1.4 -PyChef -# For paste.util.template used in keystone.common.template -Paste - -passlib -puka diff --git a/windc/tools/test-requires b/windc/tools/test-requires deleted file mode 100644 index 0e7a44da..00000000 --- a/windc/tools/test-requires +++ /dev/null @@ -1,16 +0,0 @@ -# Packages needed for dev testing -# For translations processing -Babel - -# Needed for testing -unittest2 -mock==0.8.0 -nose -nose-exclude -nosexcover -#openstack.nose_plugin -pep8==1.0.1 -sphinx>=1.1.2 -paramiko -ipaddr -pysqlite diff --git a/windc/tools/with_venv.sh b/windc/tools/with_venv.sh deleted file mode 100755 index ae91bbcb..00000000 --- a/windc/tools/with_venv.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -TOOLS=`dirname $0` -VENV=$TOOLS/../.venv -source $VENV/bin/activate && $@ diff --git a/windc/windc/__init__.py b/windc/windc/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/windc/windc/adapters/openstack.py b/windc/windc/adapters/openstack.py deleted file mode 100644 index 9ca6733d..00000000 --- a/windc/windc/adapters/openstack.py +++ /dev/null @@ -1,19 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from heatclient import Client - diff --git a/windc/windc/api/__init__.py b/windc/windc/api/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/windc/windc/api/middleware/__init__.py b/windc/windc/api/middleware/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/windc/windc/api/v1/__init__.py b/windc/windc/api/v1/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/windc/windc/api/v1/datacenters.py b/windc/windc/api/v1/datacenters.py deleted file mode 100644 index 9e050623..00000000 --- a/windc/windc/api/v1/datacenters.py +++ /dev/null @@ -1,76 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import logging - -from openstack.common import wsgi - -from windc import utils -from windc.core import api as core_api -from windc.db import api as db_api - -LOG = logging.getLogger(__name__) - - -class Datacenters_Controller(object): - def __init__(self, conf): - LOG.debug("Creating data centers controller with config:" - "datacenters.py %s", conf) - self.conf = conf - - @utils.verify_tenant - def index(self, req, tenant_id): - LOG.debug("Got index request. Request: %s", req) - result = core_api.dc_get_index(self.conf, tenant_id) - LOG.debug("Got list of datacenters: %s", result) - return {'datacenters': result} - - @utils.http_success_code(202) - @utils.verify_tenant - def create(self, req, tenant_id, body): - LOG.debug("Got create request. Request: %s", req) - #here we need to decide which device should be used - params = body.copy() - LOG.debug("Headers: %s", req.headers) - # We need to create DataCenter object and return its id - params['tenant_id'] = tenant_id - dc_id = core_api.create_dc(self.conf, params) - return {'datacenter': {'id': dc_id}} - - @utils.verify_tenant - def delete(self, req, tenant_id, datacenter_id): - LOG.debug("Got delete request. Request: %s", req) - core_api.delete_dc(self.conf, tenant_id, datacenter_id) - - @utils.verify_tenant - def show(self, req, tenant_id, datacenter_id): - LOG.debug("Got datacenter info request. Request: %s", req) - result = core_api.dc_get_data(self.conf, tenant_id, datacenter_id) - return {'datacenter': result} - - @utils.verify_tenant - def update(self, req, tenant_id, datacenter_id, body): - LOG.debug("Got update request. Request: %s", req) - core_api.update_dc(self.conf, tenant_id, datacenter_id, body) - return {'datacenter': {'id': dc_id}} - - -def create_resource(conf): - """Datacenters resource factory method""" - deserializer = wsgi.JSONRequestDeserializer() - serializer = wsgi.JSONResponseSerializer() - return wsgi.Resource(Datacenters_Controller(conf), deserializer, serializer) diff --git a/windc/windc/api/v1/filters.py b/windc/windc/api/v1/filters.py deleted file mode 100644 index 9212626a..00000000 --- a/windc/windc/api/v1/filters.py +++ /dev/null @@ -1,42 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012, Piston Cloud Computing, Inc. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -def validate(filter, value): - return FILTER_FUNCTIONS.get(filter, lambda v: True)(value) - - -def validate_int_in_range(min=0, max=None): - def _validator(v): - try: - if max is None: - return min <= int(v) - return min <= int(v) <= max - except ValueError: - return False - return _validator - - -def validate_boolean(v): - return v.lower() in ('none', 'true', 'false', '1', '0') - - -FILTER_FUNCTIONS = {'size_max': validate_int_in_range(), # build validator - 'size_min': validate_int_in_range(), # build validator - 'min_ram': validate_int_in_range(), # build validator - 'protected': validate_boolean, - 'is_public': validate_boolean, } diff --git a/windc/windc/api/v1/router.py b/windc/windc/api/v1/router.py deleted file mode 100644 index 79acc716..00000000 --- a/windc/windc/api/v1/router.py +++ /dev/null @@ -1,56 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import logging - -import routes - -from windc.api.v1 import datacenters -from windc.api.v1 import services -from openstack.common import wsgi - - -LOG = logging.getLogger(__name__) - - -class API(wsgi.Router): - - """WSGI router for windc v1 API requests.""" - - def __init__(self, conf, **local_conf): - self.conf = conf - mapper = routes.Mapper() - tenant_mapper = mapper.submapper(path_prefix="/{tenant_id}") - datacenter_resource = datacenters.create_resource(self.conf) - datacenter_collection = tenant_mapper.collection( - "datacenters", "datacenter", - controller=datacenter_resource, - member_prefix="/{datacenter_id}", - formatted=False) - service_resource = services.create_resource(self.conf) - service_collection = datacenter_collection.member.\ - collection('services','service', - controller=service_resource, - member_prefix="/{service_id}", - formatted=False) - service_collection.member.connect("/{status}", - action="changeServiceStatus", - conditions={'method': ["PUT"]}) - mapper.connect("/servicetypes", - controller=datacenter_resource, - action="show_servicetypes", - conditions={'method': ["GET"]}) - super(API, self).__init__(mapper) diff --git a/windc/windc/api/v1/services.py b/windc/windc/api/v1/services.py deleted file mode 100644 index 0a05593b..00000000 --- a/windc/windc/api/v1/services.py +++ /dev/null @@ -1,83 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import logging - -from openstack.common import wsgi - -from windc import utils -from windc.core import api as core_api -from windc.db import api as db_api - -LOG = logging.getLogger(__name__) - - -class Services_Controller(object): - def __init__(self, conf): - LOG.debug("Creating services controller with config:" - "services.py %s", conf) - self.conf = conf - - @utils.verify_tenant - def index(self, req, tenant_id, datacenter_id): - LOG.debug("Got index request. Request: %s", req) - result = core_api.service_get_index(self.conf, tenant_id, - datacenter_id) - return {'services': result} - - @utils.http_success_code(202) - @utils.verify_tenant - def create(self, req, tenant_id, datacenter_id, body): - LOG.debug("Got create request. Request: %s", req) - #here we need to decide which device should be used - params = body.copy() - LOG.debug("Headers: %s", req.headers) - # We need to create Service object and return its id - params['tenant_id'] = tenant_id - params['datacenter_id'] = datacenter_id - params['type'] = 'active_directory_service' - service_id = core_api.create_service(self.conf, params) - return {'service': {'id': service_id}} - - @utils.http_success_code(204) - @utils.verify_tenant - def delete(self, req, tenant_id, datacenter_id, service_id): - LOG.debug("Got delete request. Request: %s", req) - core_api.delete_service(self.conf, tenant_id, - datacenter_id, service_id) - - @utils.verify_tenant - def show(self, req, tenant_id, datacenter_id, service_id): - LOG.debug("Got loadbalancerr info request. Request: %s", req) - result = core_api.service_get_data(self.conf, tenant_id, - datacenter_id, service_id) - return {'service': result} - - @utils.http_success_code(202) - @utils.verify_tenant - def update(self, req, tenant_id, datacenter_id, service_id, body): - LOG.debug("Got update request. Request: %s", req) - core_api.update_service(self.conf, tenant_id, datacenter_id, - service_id, body) - return {'service': {'id': service_id}} - - -def create_resource(conf): - """Services resource factory method""" - deserializer = wsgi.JSONRequestDeserializer() - serializer = wsgi.JSONResponseSerializer() - return wsgi.Resource(Services_Controller(conf), deserializer, serializer) diff --git a/windc/windc/api/versions.py b/windc/windc/api/versions.py deleted file mode 100644 index d6516a4b..00000000 --- a/windc/windc/api/versions.py +++ /dev/null @@ -1,66 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Controller that returns information on the Glance API versions -""" - -import httplib -import json - -import webob.dec - - -class Controller(object): - - """ - A controller that produces information on the Glance API versions. - """ - - def __init__(self, conf): - self.conf = conf - - @webob.dec.wsgify - def __call__(self, req): - """Respond to a request for all OpenStack API versions.""" - version_objs = [ - { - "id": "v1.0", - "status": "CURRENT", - "links": [ - { - "rel": "self", - "href": self.get_href(req)}]}, - { - "id": "v1.1", - "status": "SUPPORTED", - "links": [ - { - "rel": "self", - "href": self.get_href(req)}]}] - - body = json.dumps(dict(versions=version_objs)) - - response = webob.Response(request=req, - status=httplib.MULTIPLE_CHOICES, - content_type='application/json') - response.body = body - - return response - - def get_href(self, req): - return "%s/v1/" % req.host_url diff --git a/windc/windc/common/__init__.py b/windc/windc/common/__init__.py deleted file mode 100644 index b6069570..00000000 --- a/windc/windc/common/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010-2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. diff --git a/windc/windc/common/cfg.py b/windc/windc/common/cfg.py deleted file mode 100644 index c63cd157..00000000 --- a/windc/windc/common/cfg.py +++ /dev/null @@ -1,1135 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Red Hat, 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. - -r""" -Configuration options which may be set on the command line or in config files. - -The schema for each option is defined using the Opt sub-classes e.g. - - common_opts = [ - cfg.StrOpt('bind_host', - default='0.0.0.0', - help='IP address to listen on'), - cfg.IntOpt('bind_port', - default=9292, - help='Port number to listen on') - ] - -Options can be strings, integers, floats, booleans, lists or 'multi strings': - - enabled_apis_opt = \ - cfg.ListOpt('enabled_apis', - default=['ec2', 'osapi'], - help='List of APIs to enable by default') - - DEFAULT_EXTENSIONS = [ - 'nova.api.openstack.contrib.standard_extensions' - ] - osapi_extension_opt = \ - cfg.MultiStrOpt('osapi_extension', - default=DEFAULT_EXTENSIONS) - -Option schemas are registered with with the config manager at runtime, but -before the option is referenced: - - class ExtensionManager(object): - - enabled_apis_opt = cfg.ListOpt(...) - - def __init__(self, conf): - self.conf = conf - self.conf.register_opt(enabled_apis_opt) - ... - - def _load_extensions(self): - for ext_factory in self.conf.osapi_extension: - .... - -A common usage pattern is for each option schema to be defined in the module or -class which uses the option: - - opts = ... - - def add_common_opts(conf): - conf.register_opts(opts) - - def get_bind_host(conf): - return conf.bind_host - - def get_bind_port(conf): - return conf.bind_port - -An option may optionally be made available via the command line. Such options -must registered with the config manager before the command line is parsed (for -the purposes of --help and CLI arg validation): - - cli_opts = [ - cfg.BoolOpt('verbose', - short='v', - default=False, - help='Print more verbose output'), - cfg.BoolOpt('debug', - short='d', - default=False, - help='Print debugging output'), - ] - - def add_common_opts(conf): - conf.register_cli_opts(cli_opts) - -The config manager has a single CLI option defined by default, --config-file: - - class ConfigOpts(object): - - config_file_opt = \ - MultiStrOpt('config-file', - ... - - def __init__(self, ...): - ... - self.register_cli_opt(self.config_file_opt) - -Option values are parsed from any supplied config files using SafeConfigParser. -If none are specified, a default set is used e.g. balancer-api.conf and -balancer-common.conf: - - balancer-api.conf: - [DEFAULT] - bind_port = 9292 - - balancer-common.conf: - [DEFAULT] - bind_host = 0.0.0.0 - -Option values in config files override those on the command line. Config files -are parsed in order, with values in later files overriding those in earlier -files. - -The parsing of CLI args and config files is initiated by invoking the config -manager e.g. - - conf = ConfigOpts() - conf.register_opt(BoolOpt('verbose', ...)) - conf(sys.argv[1:]) - if conf.verbose: - ... - -Options can be registered as belonging to a group: - - rabbit_group = cfg.OptionGroup(name='rabbit', - title='RabbitMQ options') - - rabbit_host_opt = \ - cfg.StrOpt('host', - group='rabbit', - default='localhost', - help='IP/hostname to listen on'), - rabbit_port_opt = \ - cfg.IntOpt('port', - default=5672, - help='Port number to listen on') - rabbit_ssl_opt = \ - conf.BoolOpt('use_ssl', - default=False, - help='Whether to support SSL connections') - - def register_rabbit_opts(conf): - conf.register_group(rabbit_group) - # options can be registered under a group in any of these ways: - conf.register_opt(rabbit_host_opt) - conf.register_opt(rabbit_port_opt, group='rabbit') - conf.register_opt(rabbit_ssl_opt, group=rabbit_group) - -If no group is specified, options belong to the 'DEFAULT' section of config -files: - - balancer-api.conf: - [DEFAULT] - bind_port = 9292 - ... - - [rabbit] - host = localhost - port = 5672 - use_ssl = False - userid = guest - password = guest - virtual_host = / - -Command-line options in a group are automatically prefixed with the group name: - - --rabbit-host localhost --rabbit-use-ssl False - -Option values in the default group are referenced as attributes/properties on -the config manager; groups are also attributes on the config manager, with -attributes for each of the options associated with the group: - - server.start(app, conf.bind_port, conf.bind_host, conf) - - self.connection = kombu.connection.BrokerConnection( - hostname=conf.rabbit.host, - port=conf.rabbit.port, - ...) - -Option values may reference other values using PEP 292 string substitution: - - opts = [ - cfg.StrOpt('state_path', - default=os.path.join(os.path.dirname(__file__), '../'), - help='Top-level directory for maintaining nova state'), - cfg.StrOpt('sqlite_db', - default='nova.sqlite', - help='file name for sqlite'), - cfg.StrOpt('sql_connection', - default='sqlite:///$state_path/$sqlite_db', - help='connection string for sql database'), - ] - -Note that interpolation can be avoided by using '$$'. -""" - -import sys -import ConfigParser -import copy -import optparse -import os -import string - - -class Error(Exception): - """Base class for cfg exceptions.""" - - def __init__(self, msg=None): - self.msg = msg - - def __str__(self): - return self.msg - - -class ArgsAlreadyParsedError(Error): - """Raised if a CLI opt is registered after parsing.""" - - def __str__(self): - ret = "arguments already parsed" - if self.msg: - ret += ": " + self.msg - return ret - - -class NoSuchOptError(Error): - """Raised if an opt which doesn't exist is referenced.""" - - def __init__(self, opt_name, group=None): - self.opt_name = opt_name - self.group = group - - def __str__(self): - if self.group is None: - return "no such option: %s" % self.opt_name - else: - return "no such option in group %s: %s" % (self.group.name, - self.opt_name) - - -class NoSuchGroupError(Error): - """Raised if a group which doesn't exist is referenced.""" - - def __init__(self, group_name): - self.group_name = group_name - - def __str__(self): - return "no such group: %s" % self.group_name - - -class DuplicateOptError(Error): - """Raised if multiple opts with the same name are registered.""" - - def __init__(self, opt_name): - self.opt_name = opt_name - - def __str__(self): - return "duplicate option: %s" % self.opt_name - - -class TemplateSubstitutionError(Error): - """Raised if an error occurs substituting a variable in an opt value.""" - - def __str__(self): - return "template substitution error: %s" % self.msg - - -class ConfigFilesNotFoundError(Error): - """Raised if one or more config files are not found.""" - - def __init__(self, config_files): - self.config_files = config_files - - def __str__(self): - return 'Failed to read some config files: %s' % \ - string.join(self.config_files, ',') - - -class ConfigFileParseError(Error): - """Raised if there is an error parsing a config file.""" - - def __init__(self, config_file, msg): - self.config_file = config_file - self.msg = msg - - def __str__(self): - return 'Failed to parse %s: %s' % (self.config_file, self.msg) - - -class ConfigFileValueError(Error): - """Raised if a config file value does not match its opt type.""" - pass - - -def find_config_files(project=None, prog=None, filetype="conf"): - """Return a list of default configuration files. - - We default to two config files: [${project}.conf, ${prog}.conf] - - And we look for those config files in the following directories: - - ~/.${project}/ - ~/ - /etc/${project}/ - /etc/ - - We return an absolute path for (at most) one of each the default config - files, for the topmost directory it exists in. - - For example, if project=foo, prog=bar and /etc/foo/foo.conf, /etc/bar.conf - and ~/.foo/bar.conf all exist, then we return ['/etc/foo/foo.conf', - '~/.foo/bar.conf'] - - If no project name is supplied, we only look for ${prog.conf}. - - :param project: an optional project name - :param prog: the program name, defaulting to the basename of sys.argv[0] - """ - if prog is None: - prog = os.path.basename(sys.argv[0]) - - fix_path = lambda p: os.path.abspath(os.path.expanduser(p)) - - cfg_dirs = [ - fix_path(os.path.join('~', '.' + project)) if project else None, - fix_path('~'), - os.path.join('/etc', project) if project else None, - '/etc', - 'etc', - ] - cfg_dirs = filter(bool, cfg_dirs) - - def search_dirs(dirs, basename): - for d in dirs: - path = os.path.join(d, basename) - if os.path.exists(path): - return path - - config_files = [] - - if project: - project_config = search_dirs(cfg_dirs, '%s.%s' % (project, filetype)) - config_files.append(project_config) - - config_files.append(search_dirs(cfg_dirs, '%s.%s' % (prog, filetype))) - - return filter(bool, config_files) - - -def _is_opt_registered(opts, opt): - """Check whether an opt with the same name is already registered. - - The same opt may be registered multiple times, with only the first - registration having any effect. However, it is an error to attempt - to register a different opt with the same name. - - :param opts: the set of opts already registered - :param opt: the opt to be registered - :returns: True if the opt was previously registered, False otherwise - :raises: DuplicateOptError if a naming conflict is detected - """ - if opt.dest in opts: - if opts[opt.dest]['opt'] is not opt: - raise DuplicateOptError(opt.name) - return True - else: - return False - - -class Opt(object): - - """Base class for all configuration options. - - An Opt object has no public methods, but has a number of public string - properties: - - name: - the name of the option, which may include hyphens - dest: - the (hyphen-less) ConfigOpts property which contains the option value - short: - a single character CLI option name - default: - the default value of the option - metavar: - the name shown as the argument to a CLI option in --help output - help: - an string explaining how the options value is used - """ - - def __init__(self, name, dest=None, short=None, - default=None, metavar=None, help=None): - """Construct an Opt object. - - The only required parameter is the option's name. However, it is - common to also supply a default and help string for all options. - - :param name: the option's name - :param dest: the name of the corresponding ConfigOpts property - :param short: a single character CLI option name - :param default: the default value of the option - :param metavar: the option argument to show in --help - :param help: an explanation of how the option is used - """ - self.name = name - if dest is None: - self.dest = self.name.replace('-', '_') - else: - self.dest = dest - self.short = short - self.default = default - self.metavar = metavar - self.help = help - - def _get_from_config_parser(self, cparser, section): - """Retrieves the option value from a ConfigParser object. - - This is the method ConfigOpts uses to look up the option value from - config files. Most opt types override this method in order to perform - type appropriate conversion of the returned value. - - :param cparser: a ConfigParser object - :param section: a section name - """ - return cparser.get(section, self.dest) - - def _add_to_cli(self, parser, group=None): - """Makes the option available in the command line interface. - - This is the method ConfigOpts uses to add the opt to the CLI interface - as appropriate for the opt type. Some opt types may extend this method, - others may just extend the helper methods it uses. - - :param parser: the CLI option parser - :param group: an optional OptGroup object - """ - container = self._get_optparse_container(parser, group) - kwargs = self._get_optparse_kwargs(group) - prefix = self._get_optparse_prefix('', group) - self._add_to_optparse(container, self.name, self.short, kwargs, prefix) - - def _add_to_optparse(self, container, name, short, kwargs, prefix=''): - """Add an option to an optparse parser or group. - - :param container: an optparse.OptionContainer object - :param name: the opt name - :param short: the short opt name - :param kwargs: the keyword arguments for add_option() - :param prefix: an optional prefix to prepend to the opt name - :raises: DuplicateOptError if a naming confict is detected - """ - args = ['--' + prefix + name] - if short: - args += ['-' + short] - for a in args: - if container.has_option(a): - raise DuplicateOptError(a) - container.add_option(*args, **kwargs) - - def _get_optparse_container(self, parser, group): - """Returns an optparse.OptionContainer. - - :param parser: an optparse.OptionParser - :param group: an (optional) OptGroup object - :returns: an optparse.OptionGroup if a group is given, else the parser - """ - if group is not None: - return group._get_optparse_group(parser) - else: - return parser - - def _get_optparse_kwargs(self, group, **kwargs): - """Build a dict of keyword arguments for optparse's add_option(). - - Most opt types extend this method to customize the behaviour of the - options added to optparse. - - :param group: an optional group - :param kwargs: optional keyword arguments to add to - :returns: a dict of keyword arguments - """ - dest = self.dest - if group is not None: - dest = group.name + '_' + dest - kwargs.update({ - 'dest': dest, - 'metavar': self.metavar, - 'help': self.help, - }) - return kwargs - - def _get_optparse_prefix(self, prefix, group): - """Build a prefix for the CLI option name, if required. - - CLI options in a group are prefixed with the group's name in order - to avoid conflicts between similarly named options in different - groups. - - :param prefix: an existing prefix to append to (e.g. 'no' or '') - :param group: an optional OptGroup object - :returns: a CLI option prefix including the group name, if appropriate - """ - if group is not None: - return group.name + '-' + prefix - else: - return prefix - - -class StrOpt(Opt): - """ - String opts do not have their values transformed and are returned as - str objects. - """ - pass - - -class BoolOpt(Opt): - - """ - Bool opts are set to True or False on the command line using --optname or - --noopttname respectively. - - In config files, boolean values are case insensitive and can be set using - 1/0, yes/no, true/false or on/off. - """ - - def _get_from_config_parser(self, cparser, section): - """Retrieve the opt value as a boolean from ConfigParser.""" - return cparser.getboolean(section, self.dest) - - def _add_to_cli(self, parser, group=None): - """Extends the base class method to add the --nooptname option.""" - super(BoolOpt, self)._add_to_cli(parser, group) - self._add_inverse_to_optparse(parser, group) - - def _add_inverse_to_optparse(self, parser, group): - """Add the --nooptname option to the option parser.""" - container = self._get_optparse_container(parser, group) - kwargs = self._get_optparse_kwargs(group, action='store_false') - prefix = self._get_optparse_prefix('no', group) - kwargs["help"] = "The inverse of --" + self.name - self._add_to_optparse(container, self.name, None, kwargs, prefix) - - def _get_optparse_kwargs(self, group, action='store_true', **kwargs): - """Extends the base optparse keyword dict for boolean options.""" - return super(BoolOpt, - self)._get_optparse_kwargs(group, action=action, **kwargs) - - -class IntOpt(Opt): - - """Int opt values are converted to integers using the int() builtin.""" - - def _get_from_config_parser(self, cparser, section): - """Retrieve the opt value as a integer from ConfigParser.""" - return cparser.getint(section, self.dest) - - def _get_optparse_kwargs(self, group, **kwargs): - """Extends the base optparse keyword dict for integer options.""" - return super(IntOpt, - self)._get_optparse_kwargs(group, type='int', **kwargs) - - -class FloatOpt(Opt): - - """Float opt values are converted to floats using the float() builtin.""" - - def _get_from_config_parser(self, cparser, section): - """Retrieve the opt value as a float from ConfigParser.""" - return cparser.getfloat(section, self.dest) - - def _get_optparse_kwargs(self, group, **kwargs): - """Extends the base optparse keyword dict for float options.""" - return super(FloatOpt, - self)._get_optparse_kwargs(group, type='float', **kwargs) - - -class ListOpt(Opt): - - """ - List opt values are simple string values separated by commas. The opt value - is a list containing these strings. - """ - - def _get_from_config_parser(self, cparser, section): - """Retrieve the opt value as a list from ConfigParser.""" - return cparser.get(section, self.dest).split(',') - - def _get_optparse_kwargs(self, group, **kwargs): - """Extends the base optparse keyword dict for list options.""" - return super(ListOpt, - self)._get_optparse_kwargs(group, - type='string', - action='callback', - callback=self._parse_list, - **kwargs) - - def _parse_list(self, option, opt, value, parser): - """An optparse callback for parsing an option value into a list.""" - setattr(parser.values, self.dest, value.split(',')) - - -class MultiStrOpt(Opt): - - """ - Multistr opt values are string opts which may be specified multiple times. - The opt value is a list containing all the string values specified. - """ - - def _get_from_config_parser(self, cparser, section): - """Retrieve the opt value as a multistr from ConfigParser.""" - # FIXME(markmc): values spread across the CLI and multiple - # config files should be appended - value = \ - super(MultiStrOpt, self)._get_from_config_parser(cparser, section) - return value if value is None else [value] - - def _get_optparse_kwargs(self, group, **kwargs): - """Extends the base optparse keyword dict for multi str options.""" - return super(MultiStrOpt, - self)._get_optparse_kwargs(group, action='append') - - -class OptGroup(object): - - """ - Represents a group of opts. - - CLI opts in the group are automatically prefixed with the group name. - - Each group corresponds to a section in config files. - - An OptGroup object has no public methods, but has a number of public string - properties: - - name: - the name of the group - title: - the group title as displayed in --help - help: - the group description as displayed in --help - """ - - def __init__(self, name, title=None, help=None): - """Constructs an OptGroup object. - - :param name: the group name - :param title: the group title for --help - :param help: the group description for --help - """ - self.name = name - if title is None: - self.title = "%s options" % title - else: - self.title = title - self.help = help - - self._opts = {} # dict of dicts of {opt:, override:, default:) - self._optparse_group = None - - def _register_opt(self, opt): - """Add an opt to this group. - - :param opt: an Opt object - :returns: False if previously registered, True otherwise - :raises: DuplicateOptError if a naming conflict is detected - """ - if _is_opt_registered(self._opts, opt): - return False - - self._opts[opt.dest] = {'opt': opt, 'override': None, 'default': None} - - return True - - def _get_optparse_group(self, parser): - """Build an optparse.OptionGroup for this group.""" - if self._optparse_group is None: - self._optparse_group = \ - optparse.OptionGroup(parser, self.title, self.help) - return self._optparse_group - - -class ConfigOpts(object): - - """ - Config options which may be set on the command line or in config files. - - ConfigOpts is a configuration option manager with APIs for registering - option schemas, grouping options, parsing option values and retrieving - the values of options. - """ - - def __init__(self, - project=None, - prog=None, - version=None, - usage=None, - default_config_files=None): - """Construct a ConfigOpts object. - - Automatically registers the --config-file option with either a supplied - list of default config files, or a list from find_config_files(). - - :param project: the toplevel project name, used to locate config files - :param prog: the name of the program (defaults to sys.argv[0] basename) - :param version: the program version (for --version) - :param usage: a usage string (%prog will be expanded) - :param default_config_files: config files to use by default - """ - if prog is None: - prog = os.path.basename(sys.argv[0]) - - if default_config_files is None: - default_config_files = find_config_files(project, prog) - - self.project = project - self.prog = prog - self.version = version - self.usage = usage - self.default_config_files = default_config_files - - self._opts = {} # dict of dicts of (opt:, override:, default:) - self._groups = {} - - self._args = None - self._cli_values = {} - - self._oparser = optparse.OptionParser(prog=self.prog, - version=self.version, - usage=self.usage) - self._cparser = None - - self.register_cli_opt(\ - MultiStrOpt('config-file', - default=self.default_config_files, - metavar='PATH', - help='Path to a config file to use. Multiple config ' - 'files can be specified, with values in later ' - 'files taking precedence. The default files used ' - 'are: %s' % (self.default_config_files, ))) - - def __call__(self, args=None): - """Parse command line arguments and config files. - - Calling a ConfigOpts object causes the supplied command line arguments - and config files to be parsed, causing opt values to be made available - as attributes of the object. - - The object may be called multiple times, each time causing the previous - set of values to be overwritten. - - :params args: command line arguments (defaults to sys.argv[1:]) - :returns: the list of arguments left over after parsing options - :raises: SystemExit, ConfigFilesNotFoundError, ConfigFileParseError - """ - self.reset() - - self._args = args - - (values, args) = self._oparser.parse_args(self._args) - - self._cli_values = vars(values) - - if self.config_file: - self._parse_config_files(self.config_file) - - return args - - def __getattr__(self, name): - """Look up an option value and perform string substitution. - - :param name: the opt name (or 'dest', more precisely) - :returns: the option value (after string subsititution) or a GroupAttr - :raises: NoSuchOptError,ConfigFileValueError,TemplateSubstitutionError - """ - return self._substitute(self._get(name)) - - def reset(self): - """Reset the state of the object to before it was called.""" - self._args = None - self._cli_values = None - self._cparser = None - - def register_opt(self, opt, group=None): - """Register an option schema. - - Registering an option schema makes any option value which is previously - or subsequently parsed from the command line or config files available - as an attribute of this object. - - :param opt: an instance of an Opt sub-class - :param group: an optional OptGroup object or group name - :return: False if the opt was already register, True otherwise - :raises: DuplicateOptError - """ - if group is not None: - return self._get_group(group)._register_opt(opt) - - if _is_opt_registered(self._opts, opt): - return False - - self._opts[opt.dest] = {'opt': opt, 'override': None, 'default': None} - - return True - - def register_opts(self, opts, group=None): - """Register multiple option schemas at once.""" - for opt in opts: - self.register_opt(opt, group) - - def register_cli_opt(self, opt, group=None): - """Register a CLI option schema. - - CLI option schemas must be registered before the command line and - config files are parsed. This is to ensure that all CLI options are - show in --help and option validation works as expected. - - :param opt: an instance of an Opt sub-class - :param group: an optional OptGroup object or group name - :return: False if the opt was already register, True otherwise - :raises: DuplicateOptError, ArgsAlreadyParsedError - """ - if self._args != None: - raise ArgsAlreadyParsedError("cannot register CLI option") - - if not self.register_opt(opt, group): - return False - - if group is not None: - group = self._get_group(group) - - opt._add_to_cli(self._oparser, group) - - return True - - def register_cli_opts(self, opts, group=None): - """Register multiple CLI option schemas at once.""" - for opt in opts: - self.register_cli_opt(opt, group) - - def register_group(self, group): - """Register an option group. - - An option group must be registered before options can be registered - with the group. - - :param group: an OptGroup object - """ - if group.name in self._groups: - return - - self._groups[group.name] = copy.copy(group) - - def set_override(self, name, override, group=None): - """Override an opt value. - - Override the command line, config file and default values of a - given option. - - :param name: the name/dest of the opt - :param override: the override value - :param group: an option OptGroup object or group name - :raises: NoSuchOptError, NoSuchGroupError - """ - opt_info = self._get_opt_info(name, group) - opt_info['override'] = override - - def set_default(self, name, default, group=None): - """Override an opt's default value. - - Override the default value of given option. A command line or - config file value will still take precedence over this default. - - :param name: the name/dest of the opt - :param default: the default value - :param group: an option OptGroup object or group name - :raises: NoSuchOptError, NoSuchGroupError - """ - opt_info = self._get_opt_info(name, group) - opt_info['default'] = default - - def log_opt_values(self, logger, lvl): - """Log the value of all registered opts. - - It's often useful for an app to log its configuration to a log file at - startup for debugging. This method dumps to the entire config state to - the supplied logger at a given log level. - - :param logger: a logging.Logger object - :param lvl: the log level (e.g. logging.DEBUG) arg to logger.log() - """ - logger.log(lvl, "*" * 80) - logger.log(lvl, "Configuration options gathered from:") - logger.log(lvl, "command line args: %s", self._args) - logger.log(lvl, "config files: %s", self.config_file) - logger.log(lvl, "=" * 80) - - for opt_name in sorted(self._opts): - logger.log(lvl, "%-30s = %s", opt_name, getattr(self, opt_name)) - - for group_name in self._groups: - group_attr = self.GroupAttr(self, group_name) - for opt_name in sorted(self._groups[group_name]._opts): - logger.log(lvl, "%-30s = %s", - "%s.%s" % (group_name, opt_name), - getattr(group_attr, opt_name)) - - logger.log(lvl, "*" * 80) - - def print_usage(self, file=None): - """Print the usage message for the current program.""" - self._oparser.print_usage(file) - - def _get(self, name, group=None): - """Look up an option value. - - :param name: the opt name (or 'dest', more precisely) - :param group: an option OptGroup - :returns: the option value, or a GroupAttr object - :raises: NoSuchOptError, NoSuchGroupError, ConfigFileValueError, - TemplateSubstitutionError - """ - if group is None and name in self._groups: - return self.GroupAttr(self, name) - - if group is not None: - group = self._get_group(group) - - info = self._get_opt_info(name, group) - default, opt, override = map(lambda k: info[k], sorted(info.keys())) - - if override is not None: - return override - - if self._cparser is not None: - section = group.name if group is not None else 'DEFAULT' - try: - return opt._get_from_config_parser(self._cparser, section) - except (ConfigParser.NoOptionError, - ConfigParser.NoSectionError): - pass - except ValueError, ve: - raise ConfigFileValueError(str(ve)) - - name = name if group is None else group.name + '_' + name - value = self._cli_values.get(name, None) - if value is not None: - return value - - if default is not None: - return default - - return opt.default - - def _substitute(self, value): - """Perform string template substitution. - - Substititue any template variables (e.g. $foo, ${bar}) in the supplied - string value(s) with opt values. - - :param value: the string value, or list of string values - :returns: the substituted string(s) - """ - if isinstance(value, list): - return [self._substitute(i) for i in value] - elif isinstance(value, str): - tmpl = string.Template(value) - return tmpl.safe_substitute(self.StrSubWrapper(self)) - else: - return value - - def _get_group(self, group_or_name): - """Looks up a OptGroup object. - - Helper function to return an OptGroup given a parameter which can - either be the group's name or an OptGroup object. - - The OptGroup object returned is from the internal dict of OptGroup - objects, which will be a copy of any OptGroup object that users of - the API have access to. - - :param group_or_name: the group's name or the OptGroup object itself - :raises: NoSuchGroupError - """ - if isinstance(group_or_name, OptGroup): - group_name = group_or_name.name - else: - group_name = group_or_name - - if not group_name in self._groups: - raise NoSuchGroupError(group_name) - - return self._groups[group_name] - - def _get_opt_info(self, opt_name, group=None): - """Return the (opt, override, default) dict for an opt. - - :param opt_name: an opt name/dest - :param group: an optional group name or OptGroup object - :raises: NoSuchOptError, NoSuchGroupError - """ - if group is None: - opts = self._opts - else: - group = self._get_group(group) - opts = group._opts - - if not opt_name in opts: - raise NoSuchOptError(opt_name, group) - - return opts[opt_name] - - def _parse_config_files(self, config_files): - """Parse the supplied configuration files. - - :raises: ConfigFilesNotFoundError, ConfigFileParseError - """ - self._cparser = ConfigParser.SafeConfigParser() - - try: - read_ok = self._cparser.read(config_files) - except ConfigParser.ParsingError, cpe: - raise ConfigFileParseError(cpe.filename, cpe.message) - - if read_ok != config_files: - not_read_ok = filter(lambda f: f not in read_ok, config_files) - raise ConfigFilesNotFoundError(not_read_ok) - - class GroupAttr(object): - - """ - A helper class representing the option values of a group as attributes. - """ - - def __init__(self, conf, group): - """Construct a GroupAttr object. - - :param conf: a ConfigOpts object - :param group: a group name or OptGroup object - """ - self.conf = conf - self.group = group - - def __getattr__(self, name): - """Look up an option value and perform template substitution.""" - return self.conf._substitute(self.conf._get(name, self.group)) - - class StrSubWrapper(object): - - """ - A helper class exposing opt values as a dict for string substitution. - """ - - def __init__(self, conf): - """Construct a StrSubWrapper object. - - :param conf: a ConfigOpts object - """ - self.conf = conf - - def __getitem__(self, key): - """Look up an opt value from the ConfigOpts object. - - :param key: an opt name - :returns: an opt value - :raises: TemplateSubstitutionError if attribute is a group - """ - value = getattr(self.conf, key) - if isinstance(value, self.conf.GroupAttr): - raise TemplateSubstitutionError( - 'substituting group %s not supported' % key) - return value - - -class CommonConfigOpts(ConfigOpts): - - DEFAULT_LOG_FORMAT = ('%(asctime)s %(process)d %(levelname)8s ' - '[%(name)s] %(message)s') - DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" - - common_cli_opts = [ - BoolOpt('debug', - short='d', - default=False, - help='Print debugging output'), - BoolOpt('verbose', - short='v', - default=False, - help='Print more verbose output'), - ] - - logging_cli_opts = [ - StrOpt('log-config', - metavar='PATH', - help='If this option is specified, the logging configuration ' - 'file specified is used and overrides any other logging ' - 'options specified. Please see the Python logging module ' - 'documentation for details on logging configuration ' - 'files.'), - StrOpt('log-format', - default=DEFAULT_LOG_FORMAT, - metavar='FORMAT', - help='A logging.Formatter log message format string which may ' - 'use any of the available logging.LogRecord attributes. ' - 'Default: %default'), - StrOpt('log-date-format', - default=DEFAULT_LOG_DATE_FORMAT, - metavar='DATE_FORMAT', - help='Format string for %(asctime)s in log records. ' - 'Default: %default'), - StrOpt('log-file', - metavar='PATH', - help='(Optional) Name of log file to output to. ' - 'If not set, logging will go to stdout.'), - StrOpt('log-dir', - help='(Optional) The directory to keep log files in ' - '(will be prepended to --logfile)'), - BoolOpt('use-syslog', - default=False, - help='Use syslog for logging.'), - StrOpt('syslog-log-facility', - default='LOG_USER', - help='syslog facility to receive log lines') - ] - - def __init__(self, **kwargs): - super(CommonConfigOpts, self).__init__(**kwargs) - self.register_cli_opts(self.common_cli_opts) - self.register_cli_opts(self.logging_cli_opts) diff --git a/windc/windc/common/client.py b/windc/windc/common/client.py deleted file mode 100644 index 6f383a3d..00000000 --- a/windc/windc/common/client.py +++ /dev/null @@ -1,605 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010-2011 OpenStack, LLC -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -# HTTPSClientAuthConnection code comes courtesy of ActiveState website: -# http://code.activestate.com/recipes/ -# 577548-https-httplib-client-connection-with-certificate-v/ - -import collections -import errno -import functools -import httplib -import os -import select -import urllib -import urlparse - -try: - from eventlet.green import socket, ssl -except ImportError: - import socket - import ssl - -try: - import sendfile - SENDFILE_SUPPORTED = True -except ImportError: - SENDFILE_SUPPORTED = False - -#from glance.common import auth -#from glance.common import exception, utils - - -# common chunk size for get and put -CHUNKSIZE = 65536 - - -def handle_unauthorized(func): - """ - Wrap a function to re-authenticate and retry. - """ - @functools.wraps(func) - def wrapped(self, *args, **kwargs): - try: - return func(self, *args, **kwargs) - except exception.NotAuthorized: - self._authenticate(force_reauth=True) - return func(self, *args, **kwargs) - return wrapped - - -def handle_redirects(func): - """ - Wrap the _do_request function to handle HTTP redirects. - """ - MAX_REDIRECTS = 5 - - @functools.wraps(func) - def wrapped(self, method, url, body, headers): - for _ in xrange(MAX_REDIRECTS): - try: - return func(self, method, url, body, headers) - except exception.RedirectException as redirect: - if redirect.url is None: - raise exception.InvalidRedirect() - url = redirect.url - raise exception.MaxRedirectsExceeded(redirects=MAX_REDIRECTS) - return wrapped - - -class ImageBodyIterator(object): - - """ - A class that acts as an iterator over an image file's - chunks of data. This is returned as part of the result - tuple from `glance.client.Client.get_image` - """ - - def __init__(self, source): - """ - Constructs the object from a readable image source - (such as an HTTPResponse or file-like object) - """ - self.source = source - - def __iter__(self): - """ - Exposes an iterator over the chunks of data in the - image file. - """ - while True: - chunk = self.source.read(CHUNKSIZE) - if chunk: - yield chunk - else: - break - - -class SendFileIterator: - """ - Emulate iterator pattern over sendfile, in order to allow - send progress be followed by wrapping the iteration. - """ - def __init__(self, connection, body): - self.connection = connection - self.body = body - self.offset = 0 - self.sending = True - - def __iter__(self): - class OfLength: - def __init__(self, len): - self.len = len - - def __len__(self): - return self.len - - while self.sending: - try: - sent = sendfile.sendfile(self.connection.sock.fileno(), - self.body.fileno(), - self.offset, - CHUNKSIZE) - except OSError as e: - # suprisingly, sendfile may fail transiently instead of - # blocking, in which case we select on the socket in order - # to wait on its return to a writeable state before resuming - # the send loop - if e.errno in (errno.EAGAIN, errno.EBUSY): - wlist = [self.connection.sock.fileno()] - rfds, wfds, efds = select.select([], wlist, []) - if wfds: - continue - raise - - self.sending = (sent != 0) - self.offset += sent - yield OfLength(sent) - - -class HTTPSClientAuthConnection(httplib.HTTPSConnection): - """ - Class to make a HTTPS connection, with support for - full client-based SSL Authentication - - :see http://code.activestate.com/recipes/ - 577548-https-httplib-client-connection-with-certificate-v/ - """ - - def __init__(self, host, port, key_file, cert_file, - ca_file, timeout=None, insecure=False): - httplib.HTTPSConnection.__init__(self, host, port, key_file=key_file, - cert_file=cert_file) - self.key_file = key_file - self.cert_file = cert_file - self.ca_file = ca_file - self.timeout = timeout - self.insecure = insecure - - def connect(self): - """ - Connect to a host on a given (SSL) port. - If ca_file is pointing somewhere, use it to check Server Certificate. - - Redefined/copied and extended from httplib.py:1105 (Python 2.6.x). - This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to - ssl.wrap_socket(), which forces SSL to check server certificate against - our client certificate. - """ - sock = socket.create_connection((self.host, self.port), self.timeout) - if self._tunnel_host: - self.sock = sock - self._tunnel() - # Check CA file unless 'insecure' is specificed - if self.insecure is True: - self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, - cert_reqs=ssl.CERT_NONE) - else: - self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, - ca_certs=self.ca_file, - cert_reqs=ssl.CERT_REQUIRED) - - -class BaseClient(object): - - """A base client class""" - - DEFAULT_PORT = 80 - DEFAULT_DOC_ROOT = None - # Standard CA file locations for Debian/Ubuntu, RedHat/Fedora, - # Suse, FreeBSD/OpenBSD - DEFAULT_CA_FILE_PATH = '/etc/ssl/certs/ca-certificates.crt:'\ - '/etc/pki/tls/certs/ca-bundle.crt:'\ - '/etc/ssl/ca-bundle.pem:'\ - '/etc/ssl/cert.pem' - - OK_RESPONSE_CODES = ( - httplib.OK, - httplib.CREATED, - httplib.ACCEPTED, - httplib.NO_CONTENT, - ) - - REDIRECT_RESPONSE_CODES = ( - httplib.MOVED_PERMANENTLY, - httplib.FOUND, - httplib.SEE_OTHER, - httplib.USE_PROXY, - httplib.TEMPORARY_REDIRECT, - ) - - def __init__(self, host, port=None, use_ssl=False, auth_tok=None, - creds=None, doc_root=None, key_file=None, - cert_file=None, ca_file=None, insecure=False, - configure_via_auth=True): - """ - Creates a new client to some service. - - :param host: The host where service resides - :param port: The port where service resides - :param use_ssl: Should we use HTTPS? - :param auth_tok: The auth token to pass to the server - :param creds: The credentials to pass to the auth plugin - :param doc_root: Prefix for all URLs we request from host - :param key_file: Optional PEM-formatted file that contains the private - key. - If use_ssl is True, and this param is None (the - default), then an environ variable - GLANCE_CLIENT_KEY_FILE is looked for. If no such - environ variable is found, ClientConnectionError - will be raised. - :param cert_file: Optional PEM-formatted certificate chain file. - If use_ssl is True, and this param is None (the - default), then an environ variable - GLANCE_CLIENT_CERT_FILE is looked for. If no such - environ variable is found, ClientConnectionError - will be raised. - :param ca_file: Optional CA cert file to use in SSL connections - If use_ssl is True, and this param is None (the - default), then an environ variable - GLANCE_CLIENT_CA_FILE is looked for. - :param insecure: Optional. If set then the server's certificate - will not be verified. - """ - self.host = host - self.port = port or self.DEFAULT_PORT - self.use_ssl = use_ssl - self.auth_tok = auth_tok - self.creds = creds or {} - self.connection = None - self.configure_via_auth = configure_via_auth - # doc_root can be a nullstring, which is valid, and why we - # cannot simply do doc_root or self.DEFAULT_DOC_ROOT below. - self.doc_root = (doc_root if doc_root is not None - else self.DEFAULT_DOC_ROOT) - self.auth_plugin = self.make_auth_plugin(self.creds) - - self.key_file = key_file - self.cert_file = cert_file - self.ca_file = ca_file - self.insecure = insecure - self.connect_kwargs = self.get_connect_kwargs() - - def get_connect_kwargs(self): - connect_kwargs = {} - if self.use_ssl: - if self.key_file is None: - self.key_file = os.environ.get('BALANCER_CLIENT_KEY_FILE') - if self.cert_file is None: - self.cert_file = os.environ.get('BALANCER_CLIENT_CERT_FILE') - if self.ca_file is None: - self.ca_file = os.environ.get('BALANCER_CLIENT_CA_FILE') - - # Check that key_file/cert_file are either both set or both unset - if self.cert_file is not None and self.key_file is None: - msg = _("You have selected to use SSL in connecting, " - "and you have supplied a cert, " - "however you have failed to supply either a " - "key_file parameter or set the " - "BALANCER_CLIENT_KEY_FILE environ variable") - raise exception.ClientConnectionError(msg) - - if self.key_file is not None and self.cert_file is None: - msg = _("You have selected to use SSL in connecting, " - "and you have supplied a key, " - "however you have failed to supply either a " - "cert_file parameter or set the " - "BALANCER_CLIENT_CERT_FILE environ variable") - raise exception.ClientConnectionError(msg) - - if (self.key_file is not None and - not os.path.exists(self.key_file)): - msg = _("The key file you specified %s does not " - "exist") % self.key_file - raise exception.ClientConnectionError(msg) - connect_kwargs['key_file'] = self.key_file - - if (self.cert_file is not None and - not os.path.exists(self.cert_file)): - msg = _("The cert file you specified %s does not " - "exist") % self.cert_file - raise exception.ClientConnectionError(msg) - connect_kwargs['cert_file'] = self.cert_file - - if (self.ca_file is not None and - not os.path.exists(self.ca_file)): - msg = _("The CA file you specified %s does not " - "exist") % self.ca_file - raise exception.ClientConnectionError(msg) - - if self.ca_file is None: - for ca in self.DEFAULT_CA_FILE_PATH.split(":"): - if os.path.exists(ca): - self.ca_file = ca - break - - connect_kwargs['ca_file'] = self.ca_file - connect_kwargs['insecure'] = self.insecure - - return connect_kwargs - - def set_auth_token(self, auth_tok): - """ - Updates the authentication token for this client connection. - """ - # FIXME(sirp): Nova image/glance.py currently calls this. Since this - # method isn't really doing anything useful[1], we should go ahead and - # rip it out, first in Nova, then here. Steps: - # - # 1. Change auth_tok in Glance to auth_token - # 2. Change image/glance.py in Nova to use client.auth_token - # 3. Remove this method - # - # [1] http://mail.python.org/pipermail/tutor/2003-October/025932.html - self.auth_tok = auth_tok - - def configure_from_url(self, url): - """ - Setups the connection based on the given url. - - The form is: - - ://:port/doc_root - """ - parsed = urlparse.urlparse(url) - self.use_ssl = parsed.scheme == 'https' - self.host = parsed.hostname - self.port = parsed.port or 80 - self.doc_root = parsed.path - - # ensure connection kwargs are re-evaluated after the service catalog - # publicURL is parsed for potential SSL usage - self.connect_kwargs = self.get_connect_kwargs() - - def make_auth_plugin(self, creds): - """ - Returns an instantiated authentication plugin. - """ - strategy = creds.get('strategy', 'noauth') - plugin = auth.get_plugin_from_strategy(strategy, creds) - return plugin - - def get_connection_type(self): - """ - Returns the proper connection type - """ - if self.use_ssl: - return HTTPSClientAuthConnection - else: - return httplib.HTTPConnection - - def _authenticate(self, force_reauth=False): - """ - Use the authentication plugin to authenticate and set the auth token. - - :param force_reauth: For re-authentication to bypass cache. - """ - auth_plugin = self.auth_plugin - - if not auth_plugin.is_authenticated or force_reauth: - auth_plugin.authenticate() - - self.auth_tok = auth_plugin.auth_token - - management_url = auth_plugin.management_url - if management_url and self.configure_via_auth: - self.configure_from_url(management_url) - - @handle_unauthorized - def do_request(self, method, action, body=None, headers=None, - params=None): - """ - Make a request, returning an HTTP response object. - - :param method: HTTP verb (GET, POST, PUT, etc.) - :param action: Requested path to append to self.doc_root - :param body: Data to send in the body of the request - :param headers: Headers to send with the request - :param params: Key/value pairs to use in query string - :returns: HTTP response object - """ - if not self.auth_tok: - self._authenticate() - - url = self._construct_url(action, params) - return self._do_request(method=method, url=url, body=body, - headers=headers) - - def _construct_url(self, action, params=None): - """ - Create a URL object we can use to pass to _do_request(). - """ - path = '/'.join([self.doc_root or '', action.lstrip('/')]) - scheme = "https" if self.use_ssl else "http" - netloc = "%s:%d" % (self.host, self.port) - - if isinstance(params, dict): - for (key, value) in params.items(): - if value is None: - del params[key] - query = urllib.urlencode(params) - else: - query = None - - return urlparse.ParseResult(scheme, netloc, path, '', query, '') - - @handle_redirects - def _do_request(self, method, url, body, headers): - """ - Connects to the server and issues a request. Handles converting - any returned HTTP error status codes to OpenStack/Glance exceptions - and closing the server connection. Returns the result data, or - raises an appropriate exception. - - :param method: HTTP method ("GET", "POST", "PUT", etc...) - :param url: urlparse.ParsedResult object with URL information - :param body: data to send (as string, filelike or iterable), - or None (default) - :param headers: mapping of key/value pairs to add as headers - - :note - - If the body param has a read attribute, and method is either - POST or PUT, this method will automatically conduct a chunked-transfer - encoding and use the body as a file object or iterable, transferring - chunks of data using the connection's send() method. This allows large - objects to be transferred efficiently without buffering the entire - body in memory. - """ - if url.query: - path = url.path + "?" + url.query - else: - path = url.path - - try: - connection_type = self.get_connection_type() - headers = headers or {} - - if 'x-auth-token' not in headers and self.auth_tok: - headers['x-auth-token'] = self.auth_tok - - c = connection_type(url.hostname, url.port, **self.connect_kwargs) - - def _pushing(method): - return method.lower() in ('post', 'put') - - def _simple(body): - return body is None or isinstance(body, basestring) - - def _filelike(body): - return hasattr(body, 'read') - - def _sendbody(connection, iter): - connection.endheaders() - for sent in iter: - # iterator has done the heavy lifting - pass - - def _chunkbody(connection, iter): - connection.putheader('Transfer-Encoding', 'chunked') - connection.endheaders() - for chunk in iter: - connection.send('%x\r\n%s\r\n' % (len(chunk), chunk)) - connection.send('0\r\n\r\n') - - # Do a simple request or a chunked request, depending - # on whether the body param is file-like or iterable and - # the method is PUT or POST - # - if not _pushing(method) or _simple(body): - # Simple request... - c.request(method, path, body, headers) - elif _filelike(body) or self._iterable(body): - c.putrequest(method, path) - - for header, value in headers.items(): - c.putheader(header, value) - - iter = self.image_iterator(c, headers, body) - - if self._sendable(body): - # send actual file without copying into userspace - _sendbody(c, iter) - else: - # otherwise iterate and chunk - _chunkbody(c, iter) - else: - raise TypeError('Unsupported image type: %s' % body.__class__) - - res = c.getresponse() - status_code = self.get_status_code(res) - if status_code in self.OK_RESPONSE_CODES: - return res - elif status_code in self.REDIRECT_RESPONSE_CODES: - raise exception.RedirectException(res.getheader('Location')) - elif status_code == httplib.UNAUTHORIZED: - raise exception.NotAuthorized(res.read()) - elif status_code == httplib.FORBIDDEN: - raise exception.NotAuthorized(res.read()) - elif status_code == httplib.NOT_FOUND: - raise exception.NotFound(res.read()) - elif status_code == httplib.CONFLICT: - raise exception.Duplicate(res.read()) - elif status_code == httplib.BAD_REQUEST: - raise exception.Invalid(res.read()) - elif status_code == httplib.MULTIPLE_CHOICES: - raise exception.MultipleChoices(body=res.read()) - elif status_code == httplib.INTERNAL_SERVER_ERROR: - raise Exception("Internal Server error: %s" % res.read()) - else: - raise Exception("Unknown error occurred! %s" % res.read()) - - except (socket.error, IOError), e: - raise exception.ClientConnectionError(e) - - def _seekable(self, body): - # pipes are not seekable, avoids sendfile() failure on e.g. - # cat /path/to/image | glance add ... - # or where add command is launched via popen - try: - os.lseek(body.fileno(), 0, os.SEEK_SET) - return True - except OSError as e: - return (e.errno != errno.ESPIPE) - - def _sendable(self, body): - return (SENDFILE_SUPPORTED and hasattr(body, 'fileno') and - self._seekable(body) and not self.use_ssl) - - def _iterable(self, body): - return isinstance(body, collections.Iterable) - - def image_iterator(self, connection, headers, body): - if self._sendable(body): - return SendFileIterator(connection, body) - elif self._iterable(body): - return utils.chunkreadable(body) - else: - return ImageBodyIterator(body) - - def get_status_code(self, response): - """ - Returns the integer status code from the response, which - can be either a Webob.Response (used in testing) or httplib.Response - """ - if hasattr(response, 'status_int'): - return response.status_int - else: - return response.status - - def _extract_params(self, actual_params, allowed_params): - """ - Extract a subset of keys from a dictionary. The filters key - will also be extracted, and each of its values will be returned - as an individual param. - - :param actual_params: dict of keys to filter - :param allowed_params: list of keys that 'actual_params' will be - reduced to - :retval subset of 'params' dict - """ - try: - # expect 'filters' param to be a dict here - result = dict(actual_params.get('filters')) - except TypeError: - result = {} - - for allowed_param in allowed_params: - if allowed_param in actual_params: - result[allowed_param] = actual_params[allowed_param] - - return result diff --git a/windc/windc/common/config.py b/windc/windc/common/config.py deleted file mode 100644 index 79b4e236..00000000 --- a/windc/windc/common/config.py +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env python -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Routines for configuring balancer -""" - -import logging -import logging.config -import logging.handlers -import os -import sys - -from windc.common import cfg -from windc.common import wsgi -from windc import version - - -paste_deploy_group = cfg.OptGroup('paste_deploy') -paste_deploy_opts = [ - cfg.StrOpt('flavor'), - cfg.StrOpt('config_file') - ] - - -class WindcConfigOpts(cfg.CommonConfigOpts): - def __init__(self, default_config_files=None, **kwargs): - super(WindcConfigOpts, self).__init__( - project='windc', - version='%%prog %s' % version.version_string(), - default_config_files=default_config_files, - **kwargs) - - -class WindcCacheConfigOpts(WindcConfigOpts): - - def __init__(self, **kwargs): - config_files = cfg.find_config_files(project='windc', - prog='windc-cache') - super(BalancerCacheConfigOpts, self).__init__(config_files, **kwargs) - - -def setup_logging(conf): - """ - Sets up the logging options for a log with supplied name - - :param conf: a cfg.ConfOpts object - """ - - if conf.log_config: - # Use a logging configuration file for all settings... - if os.path.exists(conf.log_config): - logging.config.fileConfig(conf.log_config) - return - else: - raise RuntimeError("Unable to locate specified logging " - "config file: %s" % conf.log_config) - - root_logger = logging.root - if conf.debug: - root_logger.setLevel(logging.DEBUG) - elif conf.verbose: - root_logger.setLevel(logging.INFO) - else: - root_logger.setLevel(logging.WARNING) - - formatter = logging.Formatter(conf.log_format, conf.log_date_format) - - if conf.use_syslog: - try: - facility = getattr(logging.handlers.SysLogHandler, - conf.syslog_log_facility) - except AttributeError: - raise ValueError(_("Invalid syslog facility")) - - handler = logging.handlers.SysLogHandler(address='/dev/log', - facility=facility) - elif conf.log_file: - logfile = conf.log_file - if conf.log_dir: - logfile = os.path.join(conf.log_dir, logfile) - handler = logging.handlers.WatchedFileHandler(logfile) - else: - handler = logging.StreamHandler(sys.stdout) - - handler.setFormatter(formatter) - root_logger.addHandler(handler) - - -def _register_paste_deploy_opts(conf): - """ - Idempotent registration of paste_deploy option group - - :param conf: a cfg.ConfigOpts object - """ - conf.register_group(paste_deploy_group) - conf.register_opts(paste_deploy_opts, group=paste_deploy_group) - - -def _get_deployment_flavor(conf): - """ - Retrieve the paste_deploy.flavor config item, formatted appropriately - for appending to the application name. - - :param conf: a cfg.ConfigOpts object - """ - _register_paste_deploy_opts(conf) - flavor = conf.paste_deploy.flavor - return '' if not flavor else ('-' + flavor) - - -def _get_deployment_config_file(conf): - """ - Retrieve the deployment_config_file config item, formatted as an - absolute pathname. - - :param conf: a cfg.ConfigOpts object - """ - _register_paste_deploy_opts(conf) - config_file = conf.paste_deploy.config_file - if not config_file: - # Assume paste config is in a paste.ini file corresponding - # to the last config file - path = conf.config_file[-1].replace(".conf", "-paste.ini") - else: - path = config_file - return os.path.abspath(path) - - -def load_paste_app(conf, app_name=None): - """ - Builds and returns a WSGI app from a paste config file. - - We assume the last config file specified in the supplied ConfigOpts - object is the paste config file. - - :param conf: a cfg.ConfigOpts object - :param app_name: name of the application to load - - :raises RuntimeError when config file cannot be located or application - cannot be loaded from config file - """ - if app_name is None: - app_name = conf.prog - - # append the deployment flavor to the application name, - # in order to identify the appropriate paste pipeline - app_name += _get_deployment_flavor(conf) - - conf_file = _get_deployment_config_file(conf) - - try: - # Setup logging early - setup_logging(conf) - - app = wsgi.paste_deploy_app(conf_file, app_name, conf) - - # Log the options used when starting if we're in debug mode... - if conf.debug: - conf.log_opt_values(logging.getLogger(app_name), logging.DEBUG) - - return app - except (LookupError, ImportError), e: - raise RuntimeError("Unable to load %(app_name)s from " - "configuration file %(conf_file)s." - "\nGot: %(e)r" % locals()) diff --git a/windc/windc/common/context.py b/windc/windc/common/context.py deleted file mode 100644 index 5a69410d..00000000 --- a/windc/windc/common/context.py +++ /dev/null @@ -1,130 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from balancer.common import cfg -from balancer.common import exception -from balancer.common import utils -from balancer.common import wsgi - - -class RequestContext(object): - """ - Stores information about the security context under which the user - accesses the system, as well as additional request information. - """ - - def __init__(self, auth_tok=None, user=None, user_id=None, tenant=None, - tenant_id=None, roles=None, is_admin=False, read_only=False, - show_deleted=False, owner_is_tenant=True): - self.auth_tok = auth_tok - self.user = user - self.user_id = user_id - self.tenant = tenant - self.tenant_id = tenant_id - self.roles = roles or [] - self.is_admin = is_admin - self.read_only = read_only - self._show_deleted = show_deleted - self.owner_is_tenant = owner_is_tenant - - @property - def owner(self): - """Return the owner to correlate with an image.""" - return self.tenant if self.owner_is_tenant else self.user - - @property - def show_deleted(self): - """Admins can see deleted by default""" - if self._show_deleted or self.is_admin: - return True - return False - - -class ContextMiddleware(wsgi.Middleware): - - opts = [ - cfg.BoolOpt('owner_is_tenant', default=True), - ] - - def __init__(self, app, conf, **local_conf): - self.conf = conf - self.conf.register_opts(self.opts) - - # Determine the context class to use - self.ctxcls = RequestContext - if 'context_class' in local_conf: - self.ctxcls = utils.import_class(local_conf['context_class']) - - super(ContextMiddleware, self).__init__(app) - - def make_context(self, *args, **kwargs): - """ - Create a context with the given arguments. - """ - kwargs.setdefault('owner_is_tenant', self.conf.owner_is_tenant) - - return self.ctxcls(*args, **kwargs) - - def process_request(self, req): - """ - Extract any authentication information in the request and - construct an appropriate context from it. - - A few scenarios exist: - - 1. If X-Auth-Token is passed in, then consult TENANT and ROLE headers - to determine permissions. - - 2. An X-Auth-Token was passed in, but the Identity-Status is not - confirmed. For now, just raising a NotAuthorized exception. - - 3. X-Auth-Token is omitted. If we were using Keystone, then the - tokenauth middleware would have rejected the request, so we must be - using NoAuth. In that case, assume that is_admin=True. - """ - # TODO(sirp): should we be using the balancer_tokeauth shim from - # Keystone here? If we do, we need to make sure it handles the NoAuth - # case - auth_tok = req.headers.get('X-Auth-Token', - req.headers.get('X-Storage-Token')) - if auth_tok: - if req.headers.get('X-Identity-Status') == 'Confirmed': - # 1. Auth-token is passed, check other headers - user = req.headers.get('X-User-Name') - user_id = req.headers.get('X-User-Id') - tenant = req.headers.get('X-Tenant-Name') - tenant_id = req.headers.get('X-Tenant-Id') - roles = [r.strip() - for r in req.headers.get('X-Role', '').split(',')] - is_admin = any(role.lower() == 'admin' for role in roles) - else: - # 2. Indentity-Status not confirmed - # FIXME(sirp): not sure what the correct behavior in this case - # is; just raising NotAuthorized for now - raise exception.NotAuthorized() - else: - # 3. Auth-token is ommited, assume NoAuth - user = None - user_id = None - tenant = None - tenant_id = None - roles = [] - is_admin = True - - req.context = self.make_context(auth_tok=auth_tok, user=user, - user_id=user_id, tenant=tenant, tenant_id=tenant_id, - roles=roles, is_admin=is_admin) diff --git a/windc/windc/common/exception.py b/windc/windc/common/exception.py deleted file mode 100644 index 24dcf619..00000000 --- a/windc/windc/common/exception.py +++ /dev/null @@ -1,184 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Glance exception subclasses""" - -import urlparse - - -class RedirectException(Exception): - def __init__(self, url): - self.url = urlparse.urlparse(url) - - -class GlanceException(Exception): - """ - Base Glance Exception - - To correctly use this class, inherit from it and define - a 'message' property. That message will get printf'd - with the keyword arguments provided to the constructor. - """ - message = "An unknown exception occurred" - - def __init__(self, *args, **kwargs): - try: - self._error_string = self.message % kwargs - except Exception: - # at least get the core message out if something happened - self._error_string = self.message - if len(args) > 0: - # If there is a non-kwarg parameter, assume it's the error - # message or reason description and tack it on to the end - # of the exception message - # Convert all arguments into their string representations... - args = ["%s" % arg for arg in args] - self._error_string = (self._error_string + - "\nDetails: %s" % '\n'.join(args)) - - def __str__(self): - return self._error_string - - -class MissingArgumentError(GlanceException): - message = "Missing required argument." - - -class MissingCredentialError(GlanceException): - message = "Missing required credential: %(required)s" - - -class BadAuthStrategy(GlanceException): - message = "Incorrect auth strategy, expected \"%(expected)s\" but " - - -class NotFound(GlanceException): - message = "An object with the specified identifier was not found." - - -class UnknownScheme(GlanceException): - message = "Unknown scheme '%(scheme)s' found in URI" - - -class BadStoreUri(GlanceException): - message = "The Store URI %(uri)s was malformed. Reason: %(reason)s" - - -class Duplicate(GlanceException): - message = "An object with the same identifier already exists." - - -class StorageFull(GlanceException): - message = "There is not enough disk space on the image storage media." - - -class StorageWriteDenied(GlanceException): - message = "Permission to write image storage media denied." - - -class ImportFailure(GlanceException): - message = "Failed to import requested object/class: '%(import_str)s'. \ - Reason: %(reason)s" - - -class AuthBadRequest(GlanceException): - message = "Connect error/bad request to Auth service at URL %(url)s." - - -class AuthUrlNotFound(GlanceException): - message = "Auth service at URL %(url)s not found." - - -class AuthorizationFailure(GlanceException): - message = "Authorization failed." - - -class NotAuthorized(GlanceException): - message = "You are not authorized to complete this action." - - -class NotAuthorizedPublicImage(NotAuthorized): - message = "You are not authorized to complete this action." - - -class Invalid(GlanceException): - message = "Data supplied was not valid." - - -class AuthorizationRedirect(GlanceException): - message = "Redirecting to %(uri)s for authorization." - - -class DatabaseMigrationError(GlanceException): - message = "There was an error migrating the database." - - -class ClientConnectionError(GlanceException): - message = "There was an error connecting to a server" - - -class ClientConfigurationError(GlanceException): - message = "There was an error configuring the client." - - -class MultipleChoices(GlanceException): - message = "The request returned a 302 Multiple Choices. This generally " - - -class InvalidContentType(GlanceException): - message = "Invalid content type %(content_type)s" - - -class BadRegistryConnectionConfiguration(GlanceException): - message = "Registry was not configured correctly on API server. " - - -class BadStoreConfiguration(GlanceException): - message = "Store %(store_name)s could not be configured correctly. " - - -class BadDriverConfiguration(GlanceException): - message = "Driver %(driver_name)s could not be configured correctly. " - - -class StoreDeleteNotSupported(GlanceException): - message = "Deleting images from this store is not supported." - - -class StoreAddDisabled(GlanceException): - message = "Configuration for store failed. Adding images to this " - - -class InvalidNotifierStrategy(GlanceException): - message = "'%(strategy)s' is not an available notifier strategy." - - -class MaxRedirectsExceeded(GlanceException): - message = "Maximum redirects (%(redirects)s) was exceeded." - - -class InvalidRedirect(GlanceException): - message = "Received invalid HTTP redirect." - - -class NoServiceEndpoint(GlanceException): - message = "Response from Keystone does not contain a Glance endpoint." - - -class RegionAmbiguity(GlanceException): - message = "Multiple 'image' service matches for region %(region)s. This " diff --git a/windc/windc/common/policy.py b/windc/windc/common/policy.py deleted file mode 100644 index 1579409e..00000000 --- a/windc/windc/common/policy.py +++ /dev/null @@ -1,182 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 OpenStack, LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Common Policy Engine Implementation""" - -import json - - -class NotAuthorized(Exception): - pass - - -_BRAIN = None - - -def set_brain(brain): - """Set the brain used by enforce(). - - Defaults use Brain() if not set. - - """ - global _BRAIN - _BRAIN = brain - - -def reset(): - """Clear the brain used by enforce().""" - global _BRAIN - _BRAIN = None - - -def enforce(match_list, target_dict, credentials_dict): - """Enforces authorization of some rules against credentials. - - :param match_list: nested tuples of data to match against - The basic brain supports three types of match lists: - 1) rules - looks like: ('rule:compute:get_instance',) - Retrieves the named rule from the rules dict and recursively - checks against the contents of the rule. - 2) roles - looks like: ('role:compute:admin',) - Matches if the specified role is in credentials_dict['roles']. - 3) generic - ('tenant_id:%(tenant_id)s',) - Substitutes values from the target dict into the match using - the % operator and matches them against the creds dict. - - Combining rules: - The brain returns True if any of the outer tuple of rules match - and also True if all of the inner tuples match. You can use this to - perform simple boolean logic. For example, the following rule would - return True if the creds contain the role 'admin' OR the if the - tenant_id matches the target dict AND the the creds contains the - role 'compute_sysadmin': - - { - "rule:combined": ( - 'role:admin', - ('tenant_id:%(tenant_id)s', 'role:compute_sysadmin') - ) - } - - - Note that rule and role are reserved words in the credentials match, so - you can't match against properties with those names. Custom brains may - also add new reserved words. For example, the HttpBrain adds http as a - reserved word. - - :param target_dict: dict of object properties - Target dicts contain as much information as we can about the object being - operated on. - - :param credentials_dict: dict of actor properties - Credentials dicts contain as much information as we can about the user - performing the action. - - :raises NotAuthorized if the check fails - - """ - global _BRAIN - if not _BRAIN: - _BRAIN = Brain() - if not _BRAIN.check(match_list, target_dict, credentials_dict): - raise NotAuthorized() - - -class Brain(object): - """Implements policy checking.""" - @classmethod - def load_json(cls, data, default_rule=None): - """Init a brain using json instead of a rules dictionary.""" - rules_dict = json.loads(data) - return cls(rules=rules_dict, default_rule=default_rule) - - def __init__(self, rules=None, default_rule=None): - self.rules = rules or {} - self.default_rule = default_rule - - def add_rule(self, key, match): - self.rules[key] = match - - def _check(self, match, target_dict, cred_dict): - match_kind, match_value = match.split(':', 1) - try: - f = getattr(self, '_check_%s' % match_kind) - except AttributeError: - if not self._check_generic(match, target_dict, cred_dict): - return False - else: - if not f(match_value, target_dict, cred_dict): - return False - return True - - def check(self, match_list, target_dict, cred_dict): - """Checks authorization of some rules against credentials. - - Detailed description of the check with examples in policy.enforce(). - - :param match_list: nested tuples of data to match against - :param target_dict: dict of object properties - :param credentials_dict: dict of actor properties - - :returns: True if the check passes - - """ - if not match_list: - return True - for and_list in match_list: - if isinstance(and_list, basestring): - and_list = (and_list,) - if all([self._check(item, target_dict, cred_dict) - for item in and_list]): - return True - return False - - def _check_rule(self, match, target_dict, cred_dict): - """Recursively checks credentials based on the brains rules.""" - try: - new_match_list = self.rules[match] - except KeyError: - if self.default_rule and match != self.default_rule: - new_match_list = ('rule:%s' % self.default_rule,) - else: - return False - - return self.check(new_match_list, target_dict, cred_dict) - - def _check_role(self, match, target_dict, cred_dict): - """Check that there is a matching role in the cred dict.""" - return match in cred_dict['roles'] - - def _check_generic(self, match, target_dict, cred_dict): - """Check an individual match. - - Matches look like: - - tenant:%(tenant_id)s - role:compute:admin - - """ - - # TODO(termie): do dict inspection via dot syntax - match = match % target_dict - key, value = match.split(':', 1) - if key in cred_dict: - return value == cred_dict[key] - return False diff --git a/windc/windc/common/utils.py b/windc/windc/common/utils.py deleted file mode 100644 index 2910816b..00000000 --- a/windc/windc/common/utils.py +++ /dev/null @@ -1,421 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -System-level utilities and helper functions. -""" - -import datetime -import errno -import logging -import os -import platform -import subprocess -import sys -import uuid - -import iso8601 - -from windc.common import exception - - -LOG = logging.getLogger(__name__) - -TIME_FORMAT = "%Y-%m-%dT%H:%M:%S" - - -class Singleton: - """ - A non-thread-safe helper class to ease implementing singletons. - This should be used as a decorator -- not a metaclass -- to the - class that should be a singleton. - - The decorated class can define one `__init__` function that - takes only the `self` argument. Other than that, there are - no restrictions that apply to the decorated class. - - To get the singleton instance, use the `Instance` method. Trying - to use `__call__` will result in a `TypeError` being raised. - - Limitations: The decorated class cannot be inherited from and the - type of the singleton instance cannot be checked with `isinstance`.. - - """ - - def __init__(self, decorated): - self._decorated = decorated - - def Instance(self, conf): - """ - Returns the singleton instance. Upon its first call, it creates a - new instance of the decorated class and calls its `__init__` method. - On all subsequent calls, the already created instance is returned. - - """ - try: - return self._instance - except AttributeError: - self._instance = self._decorated(conf) - return self._instance - - def __call__(self): - """ - Call method that raises an exception in order to prevent creation - of multiple instances of the singleton. The `Instance` method should - be used instead. - - """ - raise TypeError( - 'Singletons must be accessed through the `Instance` method.') - - -def checkNone(obj): - if bool(obj): - if obj != 'None': - return True - return False - - -def chunkreadable(iter, chunk_size=65536): - """ - Wrap a readable iterator with a reader yielding chunks of - a preferred size, otherwise leave iterator unchanged. - - :param iter: an iter which may also be readable - :param chunk_size: maximum size of chunk - """ - return chunkiter(iter, chunk_size) if hasattr(iter, 'read') else iter - - -def chunkiter(fp, chunk_size=65536): - """ - Return an iterator to a file-like obj which yields fixed size chunks - - :param fp: a file-like object - :param chunk_size: maximum size of chunk - """ - while True: - chunk = fp.read(chunk_size) - if chunk: - yield chunk - else: - break - - -def image_meta_to_http_headers(image_meta): - """ - Returns a set of image metadata into a dict - of HTTP headers that can be fed to either a Webob - Request object or an httplib.HTTP(S)Connection object - - :param image_meta: Mapping of image metadata - """ - headers = {} - for k, v in image_meta.items(): - if v is not None: - if k == 'properties': - for pk, pv in v.items(): - if pv is not None: - headers["x-image-meta-property-%s" - % pk.lower()] = unicode(pv) - else: - headers["x-image-meta-%s" % k.lower()] = unicode(v) - return headers - - -def add_features_to_http_headers(features, headers): - """ - Adds additional headers representing balancer features to be enabled. - - :param headers: Base set of headers - :param features: Map of enabled features - """ - if features: - for k, v in features.items(): - if v is not None: - headers[k.lower()] = unicode(v) - - -def get_image_meta_from_headers(response): - """ - Processes HTTP headers from a supplied response that - match the x-image-meta and x-image-meta-property and - returns a mapping of image metadata and properties - - :param response: Response to process - """ - result = {} - properties = {} - - if hasattr(response, 'getheaders'): # httplib.HTTPResponse - headers = response.getheaders() - else: # webob.Response - headers = response.headers.items() - - for key, value in headers: - key = str(key.lower()) - if key.startswith('x-image-meta-property-'): - field_name = key[len('x-image-meta-property-'):].replace('-', '_') - properties[field_name] = value or None - elif key.startswith('x-image-meta-'): - field_name = key[len('x-image-meta-'):].replace('-', '_') - result[field_name] = value or None - result['properties'] = properties - if 'size' in result: - try: - result['size'] = int(result['size']) - except ValueError: - raise exception.Invalid - for key in ('is_public', 'deleted', 'protected'): - if key in result: - result[key] = bool_from_header_value(result[key]) - return result - - -def bool_from_header_value(value): - """ - Returns True if value is a boolean True or the - string 'true', case-insensitive, False otherwise - """ - if isinstance(value, bool): - return value - elif isinstance(value, (basestring, unicode)): - if str(value).lower() == 'true': - return True - return False - - -def bool_from_string(subject): - """ - Interpret a string as a boolean. - - Any string value in: - ('True', 'true', 'On', 'on', '1') - is interpreted as a boolean True. - - Useful for JSON-decoded stuff and config file parsing - """ - if isinstance(subject, bool): - return subject - elif isinstance(subject, int): - return subject == 1 - if hasattr(subject, 'startswith'): # str or unicode... - if subject.strip().lower() in ('true', 'on', '1'): - return True - return False - - -def import_class(import_str): - """Returns a class from a string including module and class""" - mod_str, _sep, class_str = import_str.rpartition('.') - try: - __import__(mod_str) - return getattr(sys.modules[mod_str], class_str) - except (ImportError, ValueError, AttributeError), e: - raise exception.ImportFailure(import_str=import_str, - reason=e) - - -def import_object(import_str): - """Returns an object including a module or module and class""" - try: - __import__(import_str) - return sys.modules[import_str] - except ImportError: - cls = import_class(import_str) - return cls() - - -def generate_uuid(): - return str(uuid.uuid4()) - - -def is_uuid_like(value): - try: - uuid.UUID(value) - return True - except Exception: - return False - - -def isotime(at=None): - """Stringify time in ISO 8601 format""" - if not at: - at = datetime.datetime.utcnow() - str = at.strftime(TIME_FORMAT) - tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC' - str += ('Z' if tz == 'UTC' else tz) - return str - - -def parse_isotime(timestr): - """Parse time from ISO 8601 format""" - try: - return iso8601.parse_date(timestr) - except iso8601.ParseError as e: - raise ValueError(e.message) - except TypeError as e: - raise ValueError(e.message) - - -def normalize_time(timestamp): - """Normalize time in arbitrary timezone to UTC""" - offset = timestamp.utcoffset() - return timestamp.replace(tzinfo=None) - offset if offset else timestamp - - -def safe_mkdirs(path): - try: - os.makedirs(path) - except OSError, e: - if e.errno != errno.EEXIST: - raise - - -def safe_remove(path): - try: - os.remove(path) - except OSError, e: - if e.errno != errno.ENOENT: - raise - - -class PrettyTable(object): - """Creates an ASCII art table for use in bin/balancer - - Example: - - ID Name Size Hits - --- ----------------- ------------ ----- - 122 image 22 0 - """ - def __init__(self): - self.columns = [] - - def add_column(self, width, label="", just='l'): - """Add a column to the table - - :param width: number of characters wide the column should be - :param label: column heading - :param just: justification for the column, 'l' for left, - 'r' for right - """ - self.columns.append((width, label, just)) - - def make_header(self): - label_parts = [] - break_parts = [] - for width, label, _ in self.columns: - # NOTE(sirp): headers are always left justified - label_part = self._clip_and_justify(label, width, 'l') - label_parts.append(label_part) - - break_part = '-' * width - break_parts.append(break_part) - - label_line = ' '.join(label_parts) - break_line = ' '.join(break_parts) - return '\n'.join([label_line, break_line]) - - def make_row(self, *args): - row = args - row_parts = [] - for data, (width, _, just) in zip(row, self.columns): - row_part = self._clip_and_justify(data, width, just) - row_parts.append(row_part) - - row_line = ' '.join(row_parts) - return row_line - - @staticmethod - def _clip_and_justify(data, width, just): - # clip field to column width - clipped_data = str(data)[:width] - - if just == 'r': - # right justify - justified = clipped_data.rjust(width) - else: - # left justify - justified = clipped_data.ljust(width) - - return justified - - -def get_terminal_size(): - - def _get_terminal_size_posix(): - import fcntl - import struct - import termios - - height_width = None - - try: - height_width = struct.unpack('hh', fcntl.ioctl(sys.stderr.fileno(), - termios.TIOCGWINSZ, - struct.pack('HH', 0, 0))) - except: - pass - - if not height_width: - try: - p = subprocess.Popen(['stty', 'size'], - shell=False, - stdout=subprocess.PIPE) - return tuple(int(x) for x in p.communicate()[0].split()) - except: - pass - - return height_width - - def _get_terminal_size_win32(): - try: - from ctypes import windll, create_string_buffer - handle = windll.kernel32.GetStdHandle(-12) - csbi = create_string_buffer(22) - res = windll.kernel32.GetConsoleScreenBufferInfo(handle, csbi) - except: - return None - if res: - import struct - unpack_tmp = struct.unpack("hhhhHhhhhhh", csbi.raw) - (bufx, bufy, curx, cury, wattr, - left, top, right, bottom, maxx, maxy) = unpack_tmp - height = bottom - top + 1 - width = right - left + 1 - return (height, width) - else: - return None - - def _get_terminal_size_unknownOS(): - raise NotImplementedError - - func = {'posix': _get_terminal_size_posix, - 'win32': _get_terminal_size_win32} - - height_width = func.get(platform.os.name, _get_terminal_size_unknownOS)() - - if height_width == None: - raise exception.Invalid() - - for i in height_width: - if not isinstance(i, int) or i <= 0: - raise exception.Invalid() - - return height_width[0], height_width[1] diff --git a/windc/windc/common/wsgi.py b/windc/windc/common/wsgi.py deleted file mode 100644 index 3f1c6b5b..00000000 --- a/windc/windc/common/wsgi.py +++ /dev/null @@ -1,652 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# Copyright 2010 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Utility methods for working with WSGI servers -""" - -import datetime -import errno -import json -import logging -import os -import signal -import sys -import time - -import eventlet -import eventlet.greenio -from eventlet.green import socket, ssl -import eventlet.wsgi -from paste import deploy -import routes -import routes.middleware -import webob.dec -import webob.exc - -from windc.common import cfg -from windc.common import exception -from windc.common import utils - - -bind_opts = [ - cfg.StrOpt('bind_host', default='localhost'), - cfg.IntOpt('bind_port'), -] - -socket_opts = [ - cfg.IntOpt('backlog', default=4096), - cfg.StrOpt('cert_file'), - cfg.StrOpt('key_file'), -] - -workers_opt = cfg.IntOpt('workers', default=0) - - -class WritableLogger(object): - """A thin wrapper that responds to `write` and logs.""" - - def __init__(self, logger, level=logging.DEBUG): - self.logger = logger - self.level = level - - def write(self, msg): - self.logger.log(self.level, msg.strip("\n")) - - -def get_bind_addr(conf, default_port=None): - """Return the host and port to bind to.""" - conf.register_opts(bind_opts) - return (conf.bind_host, conf.bind_port or default_port) - - -def get_socket(conf, default_port): - """ - Bind socket to bind ip:port in conf - - note: Mostly comes from Swift with a few small changes... - - :param conf: a cfg.ConfigOpts object - :param default_port: port to bind to if none is specified in conf - - :returns : a socket object as returned from socket.listen or - ssl.wrap_socket if conf specifies cert_file - """ - bind_addr = get_bind_addr(conf, default_port) - - # TODO(jaypipes): eventlet's greened socket module does not actually - # support IPv6 in getaddrinfo(). We need to get around this in the - # future or monitor upstream for a fix - address_family = [addr[0] for addr in socket.getaddrinfo(bind_addr[0], - bind_addr[1], socket.AF_UNSPEC, socket.SOCK_STREAM) - if addr[0] in (socket.AF_INET, socket.AF_INET6)][0] - - conf.register_opts(socket_opts) - - cert_file = conf.cert_file - key_file = conf.key_file - use_ssl = cert_file or key_file - if use_ssl and (not cert_file or not key_file): - raise RuntimeError(_("When running server in SSL mode, you must " - "specify both a cert_file and key_file " - "option value in your configuration file")) - - sock = None - retry_until = time.time() + 30 - while not sock and time.time() < retry_until: - try: - sock = eventlet.listen(bind_addr, backlog=conf.backlog, - family=address_family) - if use_ssl: - sock = ssl.wrap_socket(sock, certfile=cert_file, - keyfile=key_file) - except socket.error, err: - if err.args[0] != errno.EADDRINUSE: - raise - eventlet.sleep(0.1) - if not sock: - raise RuntimeError(_("Could not bind to %s:%s after trying for 30 " - "seconds") % bind_addr) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - # in my experience, sockets can hang around forever without keepalive - sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - - # This option isn't available in the OS X version of eventlet - if hasattr(socket, 'TCP_KEEPIDLE'): - sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 600) - - return sock - - -class Server(object): - """Server class to manage multiple WSGI sockets and applications.""" - - def __init__(self, threads=1000): - self.threads = threads - self.children = [] - self.running = True - - def start(self, application, conf, default_port): - """ - Run a WSGI server with the given application. - - :param application: The application to run in the WSGI server - :param conf: a cfg.ConfigOpts object - :param default_port: Port to bind to if none is specified in conf - """ - def kill_children(*args): - """Kills the entire process group.""" - self.logger.error(_('SIGTERM received')) - signal.signal(signal.SIGTERM, signal.SIG_IGN) - self.running = False - os.killpg(0, signal.SIGTERM) - - def hup(*args): - """ - Shuts down the server, but allows running requests to complete - """ - self.logger.error(_('SIGHUP received')) - signal.signal(signal.SIGHUP, signal.SIG_IGN) - self.running = False - - self.application = application - self.sock = get_socket(conf, default_port) - conf.register_opt(workers_opt) - - self.logger = logging.getLogger('eventlet.wsgi.server') - - if conf.workers == 0: - # Useful for profiling, test, debug etc. - self.pool = eventlet.GreenPool(size=self.threads) - self.pool.spawn_n(self._single_run, application, self.sock) - return - - self.logger.info(_("Starting %d workers") % conf.workers) - signal.signal(signal.SIGTERM, kill_children) - signal.signal(signal.SIGHUP, hup) - while len(self.children) < conf.workers: - self.run_child() - - def wait_on_children(self): - while self.running: - try: - pid, status = os.wait() - if os.WIFEXITED(status) or os.WIFSIGNALED(status): - self.logger.error(_('Removing dead child %s') % pid) - self.children.remove(pid) - self.run_child() - except OSError, err: - if err.errno not in (errno.EINTR, errno.ECHILD): - raise - except KeyboardInterrupt: - sys.exit(1) - self.logger.info(_('Caught keyboard interrupt. Exiting.')) - break - eventlet.greenio.shutdown_safe(self.sock) - self.sock.close() - self.logger.debug(_('Exited')) - - def wait(self): - """Wait until all servers have completed running.""" - try: - if self.children: - self.wait_on_children() - else: - self.pool.waitall() - except KeyboardInterrupt: - pass - - def run_child(self): - pid = os.fork() - if pid == 0: - signal.signal(signal.SIGHUP, signal.SIG_DFL) - signal.signal(signal.SIGTERM, signal.SIG_DFL) - self.run_server() - self.logger.info(_('Child %d exiting normally') % os.getpid()) - return - else: - self.logger.info(_('Started child %s') % pid) - self.children.append(pid) - - def run_server(self): - """Run a WSGI server.""" - eventlet.wsgi.HttpProtocol.default_request_version = "HTTP/1.0" - eventlet.hubs.use_hub('poll') - eventlet.patcher.monkey_patch(all=False, socket=True) - self.pool = eventlet.GreenPool(size=self.threads) - try: - eventlet.wsgi.server(self.sock, self.application, - log=WritableLogger(self.logger), custom_pool=self.pool) - except socket.error, err: - if err[0] != errno.EINVAL: - raise - self.pool.waitall() - - def _single_run(self, application, sock): - """Start a WSGI server in a new green thread.""" - self.logger.info(_("Starting single process server")) - eventlet.wsgi.server(sock, application, custom_pool=self.pool, - log=WritableLogger(self.logger)) - - -class Middleware(object): - """ - Base WSGI middleware wrapper. These classes require an application to be - initialized that will be called next. By default the middleware will - simply call its wrapped app, or you can override __call__ to customize its - behavior. - """ - - def __init__(self, application): - self.application = application - - def process_request(self, req): - """ - Called on each request. - - If this returns None, the next application down the stack will be - executed. If it returns a response then that response will be returned - and execution will stop here. - - """ - return None - - def process_response(self, response): - """Do whatever you'd like to the response.""" - return response - - @webob.dec.wsgify - def __call__(self, req): - response = self.process_request(req) - if response: - return response - response = req.get_response(self.application) - return self.process_response(response) - - -class Debug(Middleware): - """ - Helper class that can be inserted into any WSGI application chain - to get information about the request and response. - """ - - @webob.dec.wsgify - def __call__(self, req): - print ("*" * 40) + " REQUEST ENVIRON" - for key, value in req.environ.items(): - print key, "=", value - print - resp = req.get_response(self.application) - - print ("*" * 40) + " RESPONSE HEADERS" - for (key, value) in resp.headers.iteritems(): - print key, "=", value - print - - resp.app_iter = self.print_generator(resp.app_iter) - - return resp - - @staticmethod - def print_generator(app_iter): - """ - Iterator that prints the contents of a wrapper string iterator - when iterated. - """ - print ("*" * 40) + " BODY" - for part in app_iter: - sys.stdout.write(part) - sys.stdout.flush() - yield part - print - - -class Router(object): - """ - WSGI middleware that maps incoming requests to WSGI apps. - """ - - def __init__(self, mapper): - """ - Create a router for the given routes.Mapper. - - Each route in `mapper` must specify a 'controller', which is a - WSGI app to call. You'll probably want to specify an 'action' as - well and have your controller be a wsgi.Controller, who will route - the request to the action method. - - Examples: - mapper = routes.Mapper() - sc = ServerController() - - # Explicit mapping of one route to a controller+action - mapper.connect(None, "/svrlist", controller=sc, action="list") - - # Actions are all implicitly defined - mapper.resource("server", "servers", controller=sc) - - # Pointing to an arbitrary WSGI app. You can specify the - # {path_info:.*} parameter so the target app can be handed just that - # section of the URL. - mapper.connect(None, "/v1.0/{path_info:.*}", controller=BlogApp()) - """ - self.map = mapper - self._router = routes.middleware.RoutesMiddleware(self._dispatch, - self.map) - - @webob.dec.wsgify - def __call__(self, req): - """ - Route the incoming request to a controller based on self.map. - If no match, return a 404. - """ - return self._router - - @staticmethod - @webob.dec.wsgify - def _dispatch(req): - """ - Called by self._router after matching the incoming request to a route - and putting the information into req.environ. Either returns 404 - or the routed WSGI app's response. - """ - match = req.environ['wsgiorg.routing_args'][1] - if not match: - return webob.exc.HTTPNotFound() - app = match['controller'] - return app - - -class Request(webob.Request): - """Add some Openstack API-specific logic to the base webob.Request.""" - - def best_match_content_type(self): - """Determine the requested response content-type.""" - supported = ('application/json',) - bm = self.accept.best_match(supported) - return bm or 'application/json' - - def get_content_type(self, allowed_content_types): - """Determine content type of the request body.""" - if not "Content-Type" in self.headers: - raise exception.InvalidContentType(content_type=None) - - content_type = self.content_type - - if content_type not in allowed_content_types: - raise exception.InvalidContentType(content_type=content_type) - else: - return content_type - - -class JSONRequestDeserializer(object): - def has_body(self, request): - """ - Returns whether a Webob.Request object will possess an entity body. - - :param request: Webob.Request object - """ - if 'transfer-encoding' in request.headers: - return True - elif request.content_length > 0: - return True - - return False - - def from_json(self, datastring): - return json.loads(datastring) - - def default(self, request): - if self.has_body(request): - return {'body': self.from_json(request.body)} - else: - return {} - - -class JSONResponseSerializer(object): - - def to_json(self, data): - def sanitizer(obj): - if isinstance(obj, datetime.datetime): - return obj.isoformat() - return obj - - return json.dumps(data, default=sanitizer) - - def default(self, response, result): - response.content_type = 'application/json' - response.body = self.to_json(result) - - -class Resource(object): - """ - WSGI app that handles (de)serialization and controller dispatch. - - Reads routing information supplied by RoutesMiddleware and calls - the requested action method upon its deserializer, controller, - and serializer. Those three objects may implement any of the basic - controller action methods (create, update, show, index, delete) - along with any that may be specified in the api router. A 'default' - method may also be implemented to be used in place of any - non-implemented actions. Deserializer methods must accept a request - argument and return a dictionary. Controller methods must accept a - request argument. Additionally, they must also accept keyword - arguments that represent the keys returned by the Deserializer. They - may raise a webob.exc exception or return a dict, which will be - serialized by requested content type. - """ - def __init__(self, controller, deserializer, serializer): - """ - :param controller: object that implement methods created by routes lib - :param deserializer: object that supports webob request deserialization - through controller-like actions - :param serializer: object that supports webob response serialization - through controller-like actions - """ - self.controller = controller - self.serializer = serializer - self.deserializer = deserializer - - @webob.dec.wsgify(RequestClass=Request) - def __call__(self, request): - """WSGI method that controls (de)serialization and method dispatch.""" - action_args = self.get_action_args(request.environ) - action = action_args.pop('action', None) - - deserialized_request = self.dispatch(self.deserializer, - action, request) - action_args.update(deserialized_request) - - action_result = self.dispatch(self.controller, action, - request, **action_args) - try: - response = webob.Response(request=request) - self.dispatch(self.serializer, action, response, action_result) - return response - - # return unserializable result (typically a webob exc) - except Exception: - return action_result - - def dispatch(self, obj, action, *args, **kwargs): - """Find action-specific method on self and call it.""" - try: - method = getattr(obj, action) - except AttributeError: - method = getattr(obj, 'default') - - return method(*args, **kwargs) - - def get_action_args(self, request_environment): - """Parse dictionary created by routes library.""" - try: - args = request_environment['wsgiorg.routing_args'][1].copy() - except Exception: - return {} - - try: - del args['controller'] - except KeyError: - pass - - try: - del args['format'] - except KeyError: - pass - - return args - - -class BasePasteFactory(object): - - """A base class for paste app and filter factories. - - Sub-classes must override the KEY class attribute and provide - a __call__ method. - """ - - KEY = None - - def __init__(self, conf): - self.conf = conf - - def __call__(self, global_conf, **local_conf): - raise NotImplementedError - - def _import_factory(self, local_conf): - """Import an app/filter class. - - Lookup the KEY from the PasteDeploy local conf and import the - class named there. This class can then be used as an app or - filter factory. - - Note we support the : format. - - Note also that if you do e.g. - - key = - value - - then ConfigParser returns a value with a leading newline, so - we strip() the value before using it. - """ - class_name = local_conf[self.KEY].replace(':', '.').strip() - return utils.import_class(class_name) - - -class AppFactory(BasePasteFactory): - - """A Generic paste.deploy app factory. - - This requires balancer.app_factory to be set to a callable which returns a - WSGI app when invoked. The format of the name is : e.g. - - [app:apiv1app] - paste.app_factory = balancer.common.wsgi:app_factory - balancer.app_factory = balancer.api.v1:API - - The WSGI app constructor must accept a ConfigOpts object and a local config - dict as its two arguments. - """ - print "DEBUG AppFactory start\n" - KEY = 'windc.app_factory' - - def __call__(self, global_conf, **local_conf): - """The actual paste.app_factory protocol method.""" - print "DEBUG Call factory" - factory = self._import_factory(local_conf) - return factory(self.conf, **local_conf) - - -class FilterFactory(AppFactory): - - """A Generic paste.deploy filter factory. - - This requires balancer.filter_factory to be set to a callable which returns - a WSGI filter when invoked. The format is : e.g. - - [filter:cache] - paste.filter_factory = balancer.common.wsgi:filter_factory - balancer.filter_factory = balancer.api.middleware.cache:CacheFilter - - The WSGI filter constructor must accept a WSGI app, a ConfigOpts object and - a local config dict as its three arguments. - """ - - KEY = 'windc.filter_factory' - - def __call__(self, global_conf, **local_conf): - """The actual paste.filter_factory protocol method.""" - factory = self._import_factory(local_conf) - - def filter(app): - return factory(app, self.conf, **local_conf) - - return filter - - -def setup_paste_factories(conf): - """Set up the generic paste app and filter factories. - - Set things up so that: - - paste.app_factory = balancer.common.wsgi:app_factory - - and - - paste.filter_factory = balancer.common.wsgi:filter_factory - - work correctly while loading PasteDeploy configuration. - - The app factories are constructed at runtime to allow us to pass a - ConfigOpts object to the WSGI classes. - - :param conf: a ConfigOpts object - """ - print "DEBUG Setup Factories\n" - global app_factory, filter_factory - app_factory = AppFactory(conf) - filter_factory = FilterFactory(conf) - - -def teardown_paste_factories(): - """Reverse the effect of setup_paste_factories().""" - global app_factory, filter_factory - del app_factory - del filter_factory - - -def paste_deploy_app(paste_config_file, app_name, conf): - """Load a WSGI app from a PasteDeploy configuration. - - Use deploy.loadapp() to load the app from the PasteDeploy configuration, - ensuring that the supplied ConfigOpts object is passed to the app and - filter constructors. - - :param paste_config_file: a PasteDeploy config file - :param app_name: the name of the app/pipeline to load from the file - :param conf: a ConfigOpts object to supply to the app and its filters - :returns: the WSGI app - """ - print "DEBUG Paste deploy\n" - setup_paste_factories(conf) - try: - return deploy.loadapp("config:%s" % paste_config_file, name=app_name) - finally: - teardown_paste_factories() diff --git a/windc/windc/core/__init__.py b/windc/windc/core/__init__.py deleted file mode 100644 index 1d3eb572..00000000 --- a/windc/windc/core/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import builder_set - -builder_set.builders = builder_set.BuilderSet() -#builder_set.builders.load() \ No newline at end of file diff --git a/windc/windc/core/api.py b/windc/windc/core/api.py deleted file mode 100644 index c9d1160b..00000000 --- a/windc/windc/core/api.py +++ /dev/null @@ -1,99 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from windc.db import api as db_api -from windc.core import change_events as events - -def dc_get_index(conf, tenant_id): - dcs = db_api.datacenter_get_all(conf, tenant_id) - dc_list = [db_api.unpack_extra(dc) for dc in dcs] - return dc_list - pass - -def create_dc(conf, params): - # We need to pack all attributes which are not defined by the model explicitly - dc_params = db_api.datacenter_pack_extra(params) - dc = db_api.datacenter_create(conf, dc_params) - event = events.Event(events.SCOPE_DATACENTER_CHANGE, events.ACTION_ADD) - events.change_event(conf, event, dc) - return dc.id - pass - -def delete_dc(conf, tenant_id, datacenter_id): - dc = db_api.datacenter_get(conf, tenant_id, datacenter_id) - event = events.Event(events.SCOPE_DATACENTER_CHANGE, events.ACTION_DELETE) - events.change_event(conf, event, dc) - db_api.datacenter_destroy(conf, datacenter_id) - pass - -def dc_get_data(conf, tenant_id, datacenter_id): - dc = db_api.datacenter_get(conf, tenant_id, datacenter_id) - dc_data = db_api.unpack_extra(dc) - return dc_data - pass - -def update_dc(conf, tenant_id, datacenter_id, body): - dc = db_api.datacenter_get(conf, tenant_id, datacenter_id) - old_dc = copy.deepcopy(dc) - db_api.pack_update(dc, body) - dc = db_api.datacenter_update(conf, datacenter_id, dc) - event = events.Event(events.SCOPE_DATACENTER_CHANGE, - events.ACTION_MODIFY) - event.previous_state = old_dc - events.change_event(conf, event, dc) - pass - -def service_get_index(conf, tenant_id, datacenter_id): - srvcs = db_api.service_get_all_by_datacenter_id(conf, tenant_id, - datacenter_id) - srv_list = [db_api.unpack_extra(srv) for srv in srvcs] - return srv_list - pass - -def create_service(conf, params): - # We need to pack all attributes which are not defined - # by the model explicitly - srv_params = db_api.service_pack_extra(params) - srv = db_api.service_create(conf, srv_params) - event = events.Event(events.SCOPE_SERVICE_CHANGE, events.ACTION_ADD) - events.change_event(conf, event, srv) - return srv.id - pass - -def delete_service(conf, tenant_id, datacenter_id, service_id): - srv = db_api.service_get(conf, service_id, tenant_id) - srv_data = db_api.unpack_extra(srv) - event = events.Event(events.SCOPE_SERVICE_CHANGE, events.ACTION_DELETE) - events.change_event(conf, event, srv) - db_api.service_destroy(conf,service_id) - pass - -def service_get_data(conf, tenant_id, datacenter_id, service_id): - srv = db_api.service_get(conf, service_id, tenant_id) - srv_data = db_api.unpack_extra(srv) - return srv_data - pass - -def update_service(conf, tenant_id, datacenter_id, service_id, body): - srv = db_api.service_get(conf, service_id, tenant_id) - old_srv = copy.deepcopy(srv) - db_api.pack_update(srv, body) - srv = db_api.service_update(conf, service_id, srv) - event = events.Event(events.SCOPE_SERVICE_CHANGE, events.ACTION_MODIFY) - event.previous_state = old_srv - events.change_event(conf, event, srv) - pass diff --git a/windc/windc/core/builder.py b/windc/windc/core/builder.py deleted file mode 100644 index 77088485..00000000 --- a/windc/windc/core/builder.py +++ /dev/null @@ -1,37 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -class Builder: - name = "Abstract Builder" - type = "abstract" - version = 0 - - def __init__(self, conf): - pass - - def __str__(self): - return self.name+' type: '+self.type+ ' version: ' + str(self.version) - - def build(self, context, event, data): - pass - -def create_context(): - context = {} - context['commands']=[] - return context - - diff --git a/windc/windc/core/builder_set.py b/windc/windc/core/builder_set.py deleted file mode 100644 index 0f366018..00000000 --- a/windc/windc/core/builder_set.py +++ /dev/null @@ -1,72 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from builder import Builder - -import imp -import os -import sys, glob -import logging -import traceback - -LOG = logging.getLogger(__name__) -global builders - -def load_from_file(filepath, conf): - class_inst = None - - mod_name,file_ext = os.path.splitext(os.path.split(filepath)[-1]) - - if file_ext.lower() == '.py': - py_mod = imp.load_source(mod_name, filepath) - - elif file_ext.lower() == '.pyc': - py_mod = imp.load_compiled(mod_name, filepath) - - if hasattr(py_mod, mod_name): - callable = getattr(__import__(mod_name),mod_name) - class_inst = callable(conf) - - return class_inst - - -class BuilderSet: - def __init__(self): - self.path = './windc/core/builders' - sys.path.append(self.path) - self.set = {} - - def load(self, conf): - - files = glob.glob(self.path+'/*.py') - - for file in files: - LOG.debug("Trying to load builder from file: %s", file) - try: - builder = load_from_file(file, conf) - LOG.info("Buider '%s' loaded.", builder.name) - self.set[builder.type] = builder - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - LOG.error('Can`t load builder from the file %s. Skip it.', file) - LOG.debug(repr(traceback.format_exception(exc_type, exc_value, - exc_traceback))) - - - def reload(self): - self.set = {} - self.load() diff --git a/windc/windc/core/builders/ActiveDirectory.py b/windc/windc/core/builders/ActiveDirectory.py deleted file mode 100644 index 2bae2a01..00000000 --- a/windc/windc/core/builders/ActiveDirectory.py +++ /dev/null @@ -1,210 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -import logging -import uuid -import os -from sphinx.ext.autosummary import generate - -LOG = logging.getLogger(__name__) - -from windc.core.builder import Builder -from windc.core import change_events as events -from windc.db import api as db_api -from windc.core.templates import Template -from windc.core import commands as command_api -import json -from windc.common import cfg -from random import choice - -chars = 'abcdefghklmnopqrstvwxyz2345689' - - -class ActiveDirectory(Builder): - def __init__(self, conf): - self.name = "Active Directory Builder" - self.type = "active_directory_service" - self.version = 1 - self.conf = conf - - conf.register_group(cfg.OptGroup(name="rabbitmq")) - conf.register_opts([ - cfg.StrOpt('host', default='10.0.0.1'), - cfg.StrOpt('vhost', default='keero'), - ], group="rabbitmq") - - - def build(self, context, event, data, executor): - dc = db_api.unpack_extra(data) - if event.scope == events.SCOPE_SERVICE_CHANGE: - LOG.info ("Got service change event. Analysing..") - if self.do_analysis(context, event, dc): - self.plan_changes(context, event, dc) - - self.submit_commands(context, event, dc, executor) - else: - LOG.debug("Not in my scope. Skip event.") - pass - - def generate(self, length): - return ''.join(choice(chars) for _ in range(length)) - - def do_analysis(self, context, event, data): - LOG.debug("Doing analysis for data: %s", data) - print data - - context['zones'] = ['a1'] - if data['type'] == self.type: - LOG.debug("It is a service which I should build.") - datacenter_id = data['datacenter_id'] - dc = db_api.datacenter_get(context['conf'],data['tenant_id'], - data['datacenter_id']) - datacenter = db_api.unpack_extra(dc) - context['stack_name']=datacenter['name'] - return True - else: - return False - - def plan_changes(self, context, event, data): - # Here we can plan multiple command execution. - # It might be Heat call command, then chef call command and other - # - LOG.debug("Plan changes...") - self.prepare_template(context, event, data) - # self.chef_configuration(context, event, data) - # context['commands'].append(self.deploy_template_command(context, event, data)) - # context['commands'].append(self.chef_configuration_command(context, event, data)) - pass - - def prepare_template(self, context, event, data): - LOG.debug("Prepare CloudFormation Template...") - # template = Template() - # template.add_description('Base template for Active Directory deployment') - # sec_grp = template.create_security_group('Security group for AD') - # rule = template.create_securitygroup_rule('tcp','3389','3389','0.0.0.0/0') - # template.add_rule_to_securitygroup(sec_grp, rule) - # template.add_resource('ADSecurityGroup', sec_grp) - # - # instance = template.create_instance() - # instance_name= 'AD-DC001' - # template.add_security_group(instance, 'ADSecurityGroup') - # template.add_resource(instance_name, instance) - # - # template.add_output_value(instance_name+'-IP',{"Fn::GetAtt" : [instance_name,'PublicIp']}, - # 'Public IP for the domain controller.') - - print "-------------------" - print data - print "-------------------" - print context - print "********" - try: - print self.conf.rabbitmq.vhost - except Exception, ex: - print ex - print "********" - - with open('data/Windows.template', 'r') as f: - read_data = f.read() - - template = json.loads(read_data) - - instance_template = template['Resources']['InstanceTemplate'] - - del template['Resources']['InstanceTemplate'] - context['instances'] = [] - context['template_arguments'] = { - "KeyName": "keero-linux-keys", - "InstanceType": "m1.medium", - "ImageName": "ws-2012-full-agent" - } - - for i in range(data['dc_count']): - instance_name = 'dc' + str(i) + "x" + self.generate(9) - context['instances'].append(instance_name) - template['Resources'][instance_name] = instance_template - - context['template']=template - pass - - def deploy_template_command(self, context, event, data, executor): - LOG.debug("Creating CloudFormation Template deployment command...") - #print context['template'].to_json() - LOG.debug(context['template']) - if not os.path.exists("templates"): - os.mkdir("templates") - fname = "templates/"+str(uuid.uuid4()) - print "Saving template to", fname - f=open(fname, "w") - f.write(json.dumps(context['template'])) - f.close() - context['template_name']=fname - command = command_api.Command(command_api.TEMPLATE_DEPLOYMENT_COMMAND, context) - executor.execute(command) - - def chef_configuration(self, context, event, data): - LOG.debug("Creating Chef configuration...") - context['Role'] = 'pdc' - pass - - def transform(self, path, map): - with open(path, 'r') as f: - read_data = f.read() - - template = json.loads(read_data) - if 'Commands' in template: - for command in template['Commands']: - if 'Arguments' in command: - for argument, argument_value in command['Arguments'].items(): - if isinstance(argument_value, (str, unicode)) and argument_value.startswith("@"): - command['Arguments'][argument] = map[argument_value[1:]] - - return json.dumps(template) - - def deploy_execution_plan(self, context, event, data, executor): - i = 0 - for instance in context['instances']: - i += 1 - if i == 1: - files = ["data/CreatePrimaryDC.json"] - else: - files = [] - - for file in files: - queueData = { - "queueName" : str("%s-%s" % (context['stack_name'], instance)), - "resultQueueName": "-execution-results", - "body": self.transform(file, data) - } - command = command_api.Command(command_api.EXECUTION_PLAN_DEPLOYMENT_COMMAND, context, queueData) - executor.execute(command) - - - - - def chef_configuration_command(self, context, event, data): - LOG.debug("Creating Chef configuration command...") - command = command_api.Command(command_api.CHEF_COMMAND, context) - return command - - def submit_commands(self, context, event, data, executor): - LOG.debug("Submit commands for execution...") - self.deploy_template_command(context, event, data, executor) - self.deploy_execution_plan(context, event, data, executor) - print "Commands submitted" - pass diff --git a/windc/windc/core/builders/DataCenter.py b/windc/windc/core/builders/DataCenter.py deleted file mode 100644 index d425bae7..00000000 --- a/windc/windc/core/builders/DataCenter.py +++ /dev/null @@ -1,37 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -import logging -LOG = logging.getLogger(__name__) - -from windc.core.builder import Builder -from windc.core import change_events as events - -class DataCenter(Builder): - def __init__(self, conf): - self.name = "Data Center Builder" - self.type = "datacenter" - self.version = 1 - - def build(self, context, event, data, executor): - if event.scope == events.SCOPE_DATACENTER_CHANGE: - LOG.info ("Got Data Center change event. Analysing...") - else: - LOG.debug("Not in my scope. Skip event.") - pass - diff --git a/windc/windc/core/change_events.py b/windc/windc/core/change_events.py deleted file mode 100644 index c73ea29e..00000000 --- a/windc/windc/core/change_events.py +++ /dev/null @@ -1,55 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -import logging -LOG = logging.getLogger(__name__) - -from windc.core import builder_set -from windc.core import builder -from windc.drivers import command_executor -#Declare events types - -SCOPE_SERVICE_CHANGE = "Service" -SCOPE_DATACENTER_CHANGE = "Datacenter" -SCOPE_VM_CHANGE = "VMChange" - -ACTION_ADD = "Add" -ACTION_MODIFY = "Modify" -ACTION_DELETE = "Delete" - -class Event: - scope = None - action = None - previous_state = None - def __init__(self, scope, action): - self.scope = scope - self.action = action - -def change_event(conf, event, data): - LOG.info("Change event of type: %s ", event) - context = builder.create_context() - context['conf'] = conf - executor = command_executor.Executor(conf) - for builder_type in builder_set.builders.set: - builder_instance = builder_set.builders.set[builder_type] - builder_instance.build(context, event, data, executor) - - - - - diff --git a/windc/windc/core/commands.py b/windc/windc/core/commands.py deleted file mode 100644 index 06af49c9..00000000 --- a/windc/windc/core/commands.py +++ /dev/null @@ -1,36 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -TEMPLATE_DEPLOYMENT_COMMAND = "Template" -EXECUTION_PLAN_DEPLOYMENT_COMMAND = "EPlan" -CHEF_COMMAND = "Chef" -CHEF_OP_CREATE_ENV = "Env" -CHEF_OP_CREATE_ROLE = "Role" -CHEF_OP_ASSIGN_ROLE = "AssignRole" -CHEF_OP_CREATE_NODE = "CRNode" - -class Command(object): - type = "Empty" - context = None - - - def __init__(self, type="Empty", context=None, data=None): - self.type = type - self.context = context - self.data = data - - diff --git a/windc/windc/core/service_status.py b/windc/windc/core/service_status.py deleted file mode 100644 index 564d5a12..00000000 --- a/windc/windc/core/service_status.py +++ /dev/null @@ -1,25 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# Copyright 2011 Piston Cloud Computing, Inc. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -"""Possible load balancer statuses.""" - -BUILD = "BUILD" -ACTIVE = "ACTIVE" -PENDING_UPDATE = "PENDING_UPDATE" -ERROR = "ERROR" diff --git a/windc/windc/core/templates.py b/windc/windc/core/templates.py deleted file mode 100644 index 47a7c90d..00000000 --- a/windc/windc/core/templates.py +++ /dev/null @@ -1,107 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -import logging -from windc.common.wsgi import JSONResponseSerializer -LOG = logging.getLogger(__name__) - -class Template: - def __init__(self): - self.content = {'AWSTemplateFormatVersion':'2010-09-09', 'Description':'', - 'Parameters':{}} - self.content['Mappings'] = { - "AWSInstanceType2Arch" : { - "t1.micro" : { "Arch" : "32" }, - "m1.small" : { "Arch" : "32" }, - "m1.large" : { "Arch" : "64" }, - "m1.xlarge" : { "Arch" : "64" }, - "m2.xlarge" : { "Arch" : "64" }, - "m2.2xlarge" : { "Arch" : "64" }, - "m2.4xlarge" : { "Arch" : "64" }, - "c1.medium" : { "Arch" : "32" }, - "c1.xlarge" : { "Arch" : "64" }, - "cc1.4xlarge" : { "Arch" : "64" } - }, - "DistroArch2AMI": { - "F16" : { "32" : "F16-i386-cfntools", "64" : "F16-x86_64-cfntools" }, - "F17" : { "32" : "F17-i386-cfntools", "64" : "F17-x86_64-cfntools" }, - "U10" : { "32" : "U10-i386-cfntools", "64" : "U10-x86_64-cfntools" }, - "RHEL-6.1": { "32" : "rhel61-i386-cfntools", "64" : "rhel61-x86_64-cfntools" }, - "RHEL-6.2": { "32" : "rhel62-i386-cfntools", "64" : "rhel62-x86_64-cfntools" }, - "RHEL-6.3": { "32" : "rhel63-i386-cfntools", "64" : "rhel63-x86_64-cfntools" } - } - } - self.content['Resources'] = {} - self.content['Outputs'] = {} - - def to_json(self): - serializer = JSONResponseSerializer() - json = serializer.to_json(self.content) - return json - - - def empty_template(self): - pass - - def add_description(self, description): - self.content['Description'] = description - - def add_parameter(self, name, parameter): - self.content['Parameters'].update({name : parameter}) - - def add_resource(self, name, resource): - self.content['Resources'].update({name : resource}) - - def create_parameter(self, defult, type, decription): - parameter = {'Default':default, 'Type':type, 'Description':description} - return parameter - - def create_security_group(self, description): - sec_grp = {'Type':'AWS::EC2::SecurityGroup'} - sec_grp['Properties'] = {} - sec_grp['Properties']['GroupDescription'] = description - sec_grp['Properties']['SecurityGroupIngress'] = [] - return sec_grp - - def add_rule_to_securitygroup(self, grp, rule): - grp['Properties']['SecurityGroupIngress'].append(rule) - - def create_securitygroup_rule(self, proto, f_port, t_port, cidr): - rule = {'IpProtocol':proto, 'FromPort':f_port, 'ToPort':t_port,'CidrIp': cidr} - return rule - - def create_instance(self): - instance = {'Type':'AWS::EC2::Instance','Metadata':{},'Properties':{}} - instance['Properties']['ImageId'] = 'U10-x86_64-cfntools' - instance['Properties']['SecurityGroups']=[] - instance['Properties']['KeyName'] = 'keero-linux-keys' - instance['Properties']['InstanceType'] = 'm1.small' - return instance - - def add_security_group(self, instance, grp_name): - instance['Properties']['SecurityGroups'].append({'Ref': grp_name}) - - def add_output_value(self, name, value, description): - self.content['Outputs'].update({name:{'Value':value, 'Description':description}}) - - def get_content(self): - return self.content - - - - diff --git a/windc/windc/db/__init__.py b/windc/windc/db/__init__.py deleted file mode 100644 index dd5411af..00000000 --- a/windc/windc/db/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from windc.db.api import * diff --git a/windc/windc/db/api.py b/windc/windc/db/api.py deleted file mode 100644 index 1e06f780..00000000 --- a/windc/windc/db/api.py +++ /dev/null @@ -1,164 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# Copyright 2011 Piston Cloud Computing, Inc. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -"""Database storage API.""" - -import functools -import datetime - -from windc.db import models -from windc.db.session import get_session -from windc import exception - - - -# XXX(akscram): pack_ and unpack_ are helper methods to compatibility -def pack_extra(model, values): - obj_ref = model() - pack_update(obj_ref, values) - return obj_ref - - -def unpack_extra(obj_ref): - obj_dict = dict(obj_ref.iteritems()) - obj_dict.update(obj_dict.pop('extra', None) or {}) - return obj_dict - - -def pack_update(obj_ref, values): - obj_dict = values.copy() - for k, v in values.iteritems(): - if k in obj_ref.keys(): - obj_ref[k] = obj_dict.pop(k) - if obj_dict: - if obj_ref['extra'] is not None: - obj_ref['extra'].update(obj_dict) - else: - obj_ref['extra'] = obj_dict.copy() - - -datacenter_pack_extra = functools.partial(pack_extra, models.DataCenter) -service_pack_extra = functools.partial(pack_extra, models.Service) - - -# Datacenter - - -def datacenter_get(conf, tenant_id, datacenter_id, session=None): - session = session or get_session(conf) - datacenter_ref = session.query(models.DataCenter).\ - filter_by(id=datacenter_id).first() - if not datacenter_ref: - raise exception.DeviceNotFound(datacenter_id=datacenter_id) - return datacenter_ref - - -def datacenter_get_all(conf, tenant_id): - session = get_session(conf) - query = session.query(models.DataCenter).\ - filter_by(tenant_id=tenant_id) - return query.all() - - -def datacenter_create(conf, values): - session = get_session(conf) - with session.begin(): - datacenter_ref = models.DataCenter() - datacenter_ref.update(values) - session.add(datacenter_ref) - return datacenter_ref - - -def datacenter_update(conf, datacenter_id, values): - session = get_session(conf) - with session.begin(): - datacenter_ref = session.query(models.DataCenter).\ - filter_by(id=datacenter_id).first() - datacenter_ref.update(values) - return datacenter_ref - - -def datacenter_destroy(conf, datacenter_id): - session = get_session(conf) - with session.begin(): - datacenter_ref = session.query(models.DataCenter).\ - filter_by(id=datacenter_id).first() - session.delete(datacenter_ref) - return datacenter_ref - -# Service - - -def service_get(conf, service_id, tenant_id=None, session=None): - session = session or get_session(conf) - query = session.query(models.Service).filter_by(id=service_id) - if tenant_id: - query = query.filter_by(tenant_id=tenant_id) - service_ref = query.first() - if not service_ref: - raise exception.ServiceNotFound(service_ref=service_ref) - return service_ref - -def service_get_all_by_datacenter_id(conf, tenant_id, datacenter_id): - session = get_session(conf) - query = session.query(models.Service).filter_by(datacenter_id=datacenter_id) - return query.all() - -def service_create(conf, values): - session = get_session(conf) - with session.begin(): - service_ref = models.Service() - service_ref.update(values) - session.add(service_ref) - return service_ref - -def service_update(conf, service_id, values): - session = get_session(conf) - with session.begin(): - service_ref = service_get(conf, service_id, session=session) - service_ref.update(values) - service_ref['updated_at'] = datetime.datetime.utcnow() - return service_ref - -def service_destroy(conf, service_id): - session = get_session(conf) - with session.begin(): - service_ref = service_get(conf, service_id, session=session) - session.delete(service_ref) - -def service_get_all_by_project(conf, tenant_id): - session = get_session(conf) - query = session.query(models.Service).filter_by(tenant_id=tenant_id) - return query.all() - -def service_get_all_by_vm_id(conf, tenant_id, vm_id): - session = get_session(conf) - query = session.query(models.Service).distinct().\ - filter_by(tenant_id=tenant_id).\ - filter(vm_id == vm_id) - return query.all() - -def service_count_active_by_datacenter(conf, datacenter_id): - session = get_session(conf) - with session.begin(): - service_count = session.query(models.Service).\ - filter_by(datacenter_id=datacenter_id).\ - filter_by(status=service_status.ACTIVE).\ - count() - return service_count diff --git a/windc/windc/db/base.py b/windc/windc/db/base.py deleted file mode 100644 index bf1c2f75..00000000 --- a/windc/windc/db/base.py +++ /dev/null @@ -1,76 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# Copyright 2011 Piston Cloud Computing, Inc. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -"""Base classes and custome fields for balancer models.""" - -import json - -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import object_mapper -from sqlalchemy.types import TypeDecorator -from sqlalchemy import Text - - -Base = declarative_base() - - -class DictBase(object): - def to_dict(self): - return dict(self.iteritems()) - - def __setitem__(self, key, value): - setattr(self, key, value) - - def __getitem__(self, key): - return getattr(self, key) - - def get(self, key, default=None): - return getattr(self, key, default) - - def __iter__(self): - return (col.name for col in object_mapper(self).columns) - - def keys(self): - return list(self) - - def update(self, values): - for key, value in values.iteritems(): - if isinstance(value, dict): - value = value.copy() - setattr(self, key, value) - - def iteritems(self): - items = [] - for key in self: - value = getattr(self, key) - if isinstance(value, dict): - value = value.copy() - items.append((key, value)) - return iter(items) - - -class JsonBlob(TypeDecorator): - - impl = Text - - def process_bind_param(self, value, dialect): - return json.dumps(value) - - def process_result_value(self, value, dialect): - return json.loads(value) diff --git a/windc/windc/db/migrate_repo/README b/windc/windc/db/migrate_repo/README deleted file mode 100644 index 6218f8ca..00000000 --- a/windc/windc/db/migrate_repo/README +++ /dev/null @@ -1,4 +0,0 @@ -This is a database migration repository. - -More information at -http://code.google.com/p/sqlalchemy-migrate/ diff --git a/windc/windc/db/migrate_repo/__init__.py b/windc/windc/db/migrate_repo/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/windc/windc/db/migrate_repo/manage.py b/windc/windc/db/migrate_repo/manage.py deleted file mode 100644 index 39fa3892..00000000 --- a/windc/windc/db/migrate_repo/manage.py +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env python -from migrate.versioning.shell import main - -if __name__ == '__main__': - main(debug='False') diff --git a/windc/windc/db/migrate_repo/migrate.cfg b/windc/windc/db/migrate_repo/migrate.cfg deleted file mode 100644 index 15815220..00000000 --- a/windc/windc/db/migrate_repo/migrate.cfg +++ /dev/null @@ -1,25 +0,0 @@ -[db_settings] -# Used to identify which repository this database is versioned under. -# You can use the name of your project. -repository_id=windc - -# The name of the database table used to track the schema version. -# This name shouldn't already be used by your project. -# If this is changed once a database is under version control, you'll need to -# change the table name in each database too. -version_table=migrate_version - -# When committing a change script, Migrate will attempt to generate the -# sql for all supported databases; normally, if one of them fails - probably -# because you don't have that database installed - it is ignored and the -# commit continues, perhaps ending successfully. -# Databases in this list MUST compile successfully during a commit, or the -# entire commit will fail. List the databases your application will actually -# be using to ensure your updates to that database work properly. -# This must be a list; example: ['postgres','sqlite'] -required_dbs=[] - -# When creating new change scripts, Migrate will stamp the new script with -# a version number. By default this is latest_version + 1. You can set this -# to 'true' to tell Migrate to use the UTC timestamp instead. -use_timestamp_numbering=False diff --git a/windc/windc/db/migrate_repo/versions/001_Add_initial_tables.py b/windc/windc/db/migrate_repo/versions/001_Add_initial_tables.py deleted file mode 100644 index 12003287..00000000 --- a/windc/windc/db/migrate_repo/versions/001_Add_initial_tables.py +++ /dev/null @@ -1,41 +0,0 @@ -from sqlalchemy.schema import MetaData, Table, Column, ForeignKey -from sqlalchemy.types import Integer, String, Text, DateTime - - -meta = MetaData() - -Table('datacenter', meta, - Column('id', String(32), primary_key=True), - Column('name', String(255)), - Column('type', String(255)), - Column('version', String(255)), - Column('tenant_id',String(100)), - Column('KMS', String(80)), - Column('WSUS', String(80)), - Column('extra', Text()), -) - -Table('service', meta, - Column('id', String(32), primary_key=True), - Column('datacenter_id', String(32), ForeignKey('datacenter.id')), - Column('name', String(255)), - Column('type', String(40)), - Column('status', String(255)), - Column('tenant_id', String(40)), - Column('created_at', DateTime, nullable=False), - Column('updated_at', DateTime, nullable=False), - Column('deployed', String(40)), - Column('vm_id',String(40)), - Column('extra', Text()), -) - - - -def upgrade(migrate_engine): - meta.bind = migrate_engine - meta.create_all() - - -def downgrade(migrate_engine): - meta.bind = migrate_engine - meta.drop_all() diff --git a/windc/windc/db/migrate_repo/versions/__init__.py b/windc/windc/db/migrate_repo/versions/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/windc/windc/db/models.py b/windc/windc/db/models.py deleted file mode 100644 index aa755ab9..00000000 --- a/windc/windc/db/models.py +++ /dev/null @@ -1,81 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -#Copyright by Mirantis 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. -"""SQLAlchemy models for balancer data.""" - -import datetime -import uuid - -from sqlalchemy.orm import relationship, backref -from sqlalchemy import (Column, ForeignKey, Integer, String, Boolean, - DateTime) - -from windc.db.base import Base, DictBase, JsonBlob - - -def create_uuid(): - return uuid.uuid4().hex - - -class DataCenter(DictBase, Base): - """ - Represents a data center - a Windows Environment with different - services in it. - """ - - __tablename__ = 'datacenter' - id = Column(String(32), primary_key=True, default=create_uuid) - name = Column(String(255)) - type = Column(String(255)) - version = Column(String(255)) - tenant_id = Column(String(100)) - KMS = Column(String(80)) - WSUS = Column(String(80)) - extra = Column(JsonBlob()) - - -class Service(DictBase, Base): - """ - Represents an instance of service. - - :var name: string - :var type: string - type of service (e.g. Active Directory) - :var tenant_id: string - OpenStack tenant ID - :var extra: dictionary - additional attributes - """ - - __tablename__ = 'service' - id = Column(String(32), primary_key=True, default=create_uuid) - datacenter_id = Column(String(32), ForeignKey('datacenter.id')) - name = Column(String(255)) - type = Column(String(40)) - status = Column(String(40)) - tenant_id = Column(String(40)) - created_at = Column(DateTime, default=datetime.datetime.utcnow, - nullable=False) - updated_at = Column(DateTime, default=datetime.datetime.utcnow, - onupdate=datetime.datetime.utcnow, - nullable=False) - deployed = Column(String(40)) - vm_id = Column(String(40)) - extra = Column(JsonBlob()) - datacenter = relationship(DataCenter, - backref=backref('service', order_by=id), - uselist=False) - -def register_models(engine): - """Create tables for models.""" - - Base.metadata.create_all(engine) diff --git a/windc/windc/db/session.py b/windc/windc/db/session.py deleted file mode 100644 index 19161768..00000000 --- a/windc/windc/db/session.py +++ /dev/null @@ -1,122 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# Copyright 2011 Piston Cloud Computing, Inc. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -"""Session management functions.""" - -import os -import logging - -from migrate.versioning import api as versioning_api -from migrate import exceptions as versioning_exceptions -from sqlalchemy import create_engine -from sqlalchemy.engine.url import make_url -from sqlalchemy.orm import sessionmaker -from sqlalchemy.pool import NullPool -from sqlalchemy.exc import DisconnectionError - -from windc.common import cfg -from windc.db import migrate_repo - - -DB_GROUP_NAME = 'sql' -DB_OPTIONS = ( - cfg.IntOpt('idle_timeout', default=3600), - cfg.StrOpt('connection', default='sqlite:///windc.sqlite'), -) - -MAKER = None -ENGINE = None - - -class MySQLPingListener(object): - """ - Ensures that MySQL connections checked out of the - pool are alive. - - Borrowed from: - http://groups.google.com/group/sqlalchemy/msg/a4ce563d802c929f - - Error codes caught: - * 2006 MySQL server has gone away - * 2013 Lost connection to MySQL server during query - * 2014 Commands out of sync; you can't run this command now - * 2045 Can't open shared memory; no answer from server (%lu) - * 2055 Lost connection to MySQL server at '%s', system error: %d - - from http://dev.mysql.com/doc/refman/5.6/en/error-messages-client.html - """ - - def checkout(self, dbapi_con, con_record, con_proxy): - try: - dbapi_con.cursor().execute('select 1') - except dbapi_con.OperationalError, ex: - if ex.args[0] in (2006, 2013, 2014, 2045, 2055): - logging.warn('Got mysql server has gone away: %s', ex) - raise DisconnectionError("Database server went away") - else: - raise - - -def get_session(conf, autocommit=True, expire_on_commit=False): - """Return a SQLAlchemy session.""" - global MAKER - - if MAKER is None: - MAKER = sessionmaker(autocommit=autocommit, - expire_on_commit=expire_on_commit) - engine = get_engine(conf) - MAKER.configure(bind=engine) - session = MAKER() - return session - - -def get_engine(conf): - """Return a SQLAlchemy engine.""" - global ENGINE - - register_conf_opts(conf) - connection_url = make_url(conf.sql.connection) - if ENGINE is None or not ENGINE.url == connection_url: - engine_args = {'pool_recycle': conf.sql.idle_timeout, - 'echo': False, - 'convert_unicode': True - } - if 'sqlite' in connection_url.drivername: - engine_args['poolclass'] = NullPool - if 'mysql' in connection_url.drivername: - engine_args['listeners'] = [MySQLPingListener()] - ENGINE = create_engine(conf.sql.connection, **engine_args) - return ENGINE - - -def register_conf_opts(conf, options=DB_OPTIONS, group=DB_GROUP_NAME): - """Register database options.""" - - conf.register_group(cfg.OptGroup(name=group)) - conf.register_opts(options, group=group) - - -def sync(conf): - register_conf_opts(conf) - repo_path = os.path.abspath(os.path.dirname(migrate_repo.__file__)) - try: - versioning_api.upgrade(conf.sql.connection, repo_path) - except versioning_exceptions.DatabaseNotControlledError: - versioning_api.version_control(conf.sql.connection, repo_path) - versioning_api.upgrade(conf.sql.connection, repo_path) diff --git a/windc/windc/drivers/__init__.py b/windc/windc/drivers/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/windc/windc/drivers/command_executor.py b/windc/windc/drivers/command_executor.py deleted file mode 100644 index f675a0ab..00000000 --- a/windc/windc/drivers/command_executor.py +++ /dev/null @@ -1,40 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# Copyright 2011 Piston Cloud Computing, Inc. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from windc.core import commands as commands_api -from windc.drivers import openstack_heat -from windc.drivers import windows_agent - -class Executor: - - map = {commands_api.TEMPLATE_DEPLOYMENT_COMMAND : openstack_heat.Heat} - - def __init__(self, conf): - self._conf = conf - - def execute(self, command): - if command.type == commands_api.TEMPLATE_DEPLOYMENT_COMMAND: - executor = openstack_heat.Heat() - return executor.execute(command) - elif command.type == commands_api.EXECUTION_PLAN_DEPLOYMENT_COMMAND: - executor = windows_agent.Agent(self._conf) - return executor.execute(command) - - diff --git a/windc/windc/drivers/openstack_heat.py b/windc/windc/drivers/openstack_heat.py deleted file mode 100644 index dd50fda8..00000000 --- a/windc/windc/drivers/openstack_heat.py +++ /dev/null @@ -1,43 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# Copyright 2011 Piston Cloud Computing, Inc. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -#from heatclient import Client -from subprocess import call - -import logging -LOG = logging.getLogger(__name__) - -class Heat: - - def __init__(self): - pass - - def execute(self, command): -# client = Client('1',OS_IMAGE_ENDPOINT, OS_TENANT_ID) - LOG.debug('Calling heat script to execute template') - arguments = ";".join(['%s=%s' % (key, value) for (key, value) in command.context['template_arguments'].items()]) - call([ - "./heat_run","stack-create", - "-f" + command.context['template_name'], - "-P" + arguments, - command.context['stack_name'] - ]) - pass - diff --git a/windc/windc/drivers/windows_agent.py b/windc/windc/drivers/windows_agent.py deleted file mode 100644 index e446b05e..00000000 --- a/windc/windc/drivers/windows_agent.py +++ /dev/null @@ -1,66 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# Copyright 2011 Piston Cloud Computing, Inc. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -import traceback - -import puka - -import logging -import sys - -LOG = logging.getLogger(__name__) - -class Agent(object): - - def __init__(self, conf): - self._conf = conf - - def execute(self, command): - try: - client = puka.Client("amqp://keero:keero@%s/%s" % ( - self._conf.rabbitmq.host, self._conf.rabbitmq.vhost)) - promise = client.connect() - client.wait(promise) - - - promise = client.queue_declare(queue=command.data['queueName'], durable=True) - client.wait(promise) - - promise = client.queue_declare(queue=command.data['resultQueueName'], durable=True) - client.wait(promise) - - promise = client.basic_publish(exchange='', routing_key=command.data['queueName'], - body=command.data['body']) - client.wait(promise) - - consume_promise = client.basic_consume(queue=command.data['resultQueueName']) - result = client.wait(consume_promise) - - result_msg = result['body'] - client.basic_ack(result) - client.basic_cancel(consume_promise) - - promise = client.close() - client.wait(promise) - - return result_msg - except Exception: - exc_type, exc_value, exc_traceback = sys.exc_info() - print exc_type, exc_value, exc_traceback - print traceback.format_exc() diff --git a/windc/windc/exception.py b/windc/windc/exception.py deleted file mode 100644 index 3144b6ed..00000000 --- a/windc/windc/exception.py +++ /dev/null @@ -1,50 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# Copyright 2011 Piston Cloud Computing, Inc. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -"""Balancer base exception handling.""" - -import webob.exc as exception - - -class NotFound(exception.HTTPNotFound): - message = 'Resource not found.' - - def __init__(self, message=None, **kwargs): - super(NotFound, self).__init__(message) - self.kwargs = kwargs - - -class DeviceNotFound(NotFound): - message = 'Device not found' - - -class NoValidDevice(NotFound): - message = 'Suitable device not found' - - -class ServiceNotFound(NotFound): - message = 'LoadBalancer not found' - - -class DeviceConflict(exception.HTTPConflict): - message = 'Conflict while device deleting' - - def __init__(self, message=None, **kwargs): - super(DeviceConflict, self).__init__(message) - self.kwargs = kwargs diff --git a/windc/windc/utils.py b/windc/windc/utils.py deleted file mode 100644 index c0531726..00000000 --- a/windc/windc/utils.py +++ /dev/null @@ -1,66 +0,0 @@ -import contextlib -import functools -import logging -import sys - -import webob.exc - -LOG = logging.getLogger(__name__) - - -def http_success_code(code): - """Attaches response code to a method. - - This decorator associates a response code with a method. Note - that the function attributes are directly manipulated; the method - is not wrapped. - """ - - def decorator(func): - func.wsgi_code = code - return func - return decorator - - -def verify_tenant(func): - @functools.wraps(func) - def __inner(self, req, tenant_id, *args, **kwargs): - if hasattr(req, 'context') and tenant_id != req.context.tenant_id: - LOG.info('User is not authorized to access this tenant.') - raise webob.exc.HTTPUnauthorized - return func(self, req, tenant_id, *args, **kwargs) - return __inner - - -def require_admin(func): - @functools.wraps(func) - def __inner(self, req, *args, **kwargs): - if hasattr(req, 'context') and not req.context.is_admin: - LOG.info('User has no admin priviledges.') - raise webob.exc.HTTPUnauthorized - return func(self, req, *args, **kwargs) - return __inner - - -@contextlib.contextmanager -def save_and_reraise_exception(): - """Save current exception, run some code and then re-raise. - - In some cases the exception context can be cleared, resulting in None - being attempted to be reraised after an exception handler is run. This - can happen when eventlet switches greenthreads or when running an - exception handler, code raises and catches an exception. In both - cases the exception context will be cleared. - - To work around this, we save the exception state, run handler code, and - then re-raise the original exception. If another exception occurs, the - saved exception is logged and the new exception is reraised. - """ - type_, value, traceback = sys.exc_info() - try: - yield - except Exception: - LOG.error('Original exception being dropped', - exc_info=(type_, value, traceback)) - raise - raise type_, value, traceback diff --git a/windc/windc/version.py b/windc/windc/version.py deleted file mode 100644 index 943c019a..00000000 --- a/windc/windc/version.py +++ /dev/null @@ -1,49 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Determine version of Skeleton library""" - -try: - from skeleton.vcsversion import version_info -except ImportError: - version_info = {'branch_nick': u'LOCALBRANCH', - 'revision_id': 'LOCALREVISION', - 'revno': 0} - -SKELETON_VERSION = ['2011', '3'] -YEAR, COUNT = SKELETON_VERSION - -FINAL = False # This becomes true at Release Candidate time - - -def canonical_version_string(): - return '.'.join([YEAR, COUNT]) - - -def version_string(): - if FINAL: - return canonical_version_string() - else: - return '%s-dev' % (canonical_version_string(),) - - -def vcs_version_string(): - return "%s:%s" % (version_info['branch_nick'], version_info['revision_id']) - - -def version_string_with_vcs(): - return "%s-%s" % (canonical_version_string(), vcs_version_string()) From 267724335740497e6d902befa0890b4c4d953722 Mon Sep 17 00:00:00 2001 From: Serg Melikyan Date: Mon, 18 Mar 2013 15:40:16 +0400 Subject: [PATCH 002/135] Cloned horizon and added our dashboard --- dashboard/.gitignore | 28 + dashboard/.gitreview | 4 + dashboard/.mailmap | 10 + dashboard/.pep8 | 4 + dashboard/.pylintrc | 42 + dashboard/.tx/config | 11 + dashboard/LICENSE | 176 + dashboard/MANIFEST.in | 22 + dashboard/Makefile | 24 + dashboard/README.rst | 125 + dashboard/ReadMe.txt | 31 - dashboard/bin/less/lessc | 139 + dashboard/bin/lib/less/browser.js | 380 ++ dashboard/bin/lib/less/colors.js | 152 + dashboard/bin/lib/less/cssmin.js | 355 ++ dashboard/bin/lib/less/functions.js | 228 + dashboard/bin/lib/less/index.js | 148 + dashboard/bin/lib/less/parser.js | 1334 +++++ dashboard/bin/lib/less/rhino.js | 62 + dashboard/bin/lib/less/tree.js | 17 + dashboard/bin/lib/less/tree/alpha.js | 17 + dashboard/bin/lib/less/tree/anonymous.js | 13 + dashboard/bin/lib/less/tree/assignment.js | 17 + dashboard/bin/lib/less/tree/call.js | 48 + dashboard/bin/lib/less/tree/color.js | 101 + dashboard/bin/lib/less/tree/comment.js | 14 + dashboard/bin/lib/less/tree/condition.js | 42 + dashboard/bin/lib/less/tree/dimension.js | 49 + dashboard/bin/lib/less/tree/directive.js | 35 + dashboard/bin/lib/less/tree/element.js | 52 + dashboard/bin/lib/less/tree/expression.js | 23 + dashboard/bin/lib/less/tree/import.js | 83 + dashboard/bin/lib/less/tree/javascript.js | 51 + dashboard/bin/lib/less/tree/keyword.js | 19 + dashboard/bin/lib/less/tree/media.js | 114 + dashboard/bin/lib/less/tree/mixin.js | 146 + dashboard/bin/lib/less/tree/operation.js | 32 + dashboard/bin/lib/less/tree/paren.js | 16 + dashboard/bin/lib/less/tree/quoted.js | 29 + dashboard/bin/lib/less/tree/rule.js | 42 + dashboard/bin/lib/less/tree/ruleset.js | 225 + dashboard/bin/lib/less/tree/selector.js | 42 + dashboard/bin/lib/less/tree/url.js | 25 + dashboard/bin/lib/less/tree/value.js | 24 + dashboard/bin/lib/less/tree/variable.js | 26 + dashboard/doc/Makefile | 153 + .../source/_static/.gitignore} | 0 dashboard/doc/source/_static/basic.css | 416 ++ dashboard/doc/source/_static/default.css | 230 + dashboard/doc/source/_static/header-line.gif | Bin 0 -> 48 bytes dashboard/doc/source/_static/header_bg.jpg | Bin 0 -> 3738 bytes dashboard/doc/source/_static/jquery.tweet.js | 154 + dashboard/doc/source/_static/nature.css | 245 + .../doc/source/_static/openstack_logo.png | Bin 0 -> 3670 bytes dashboard/doc/source/_static/tweaks.css | 94 + dashboard/doc/source/_templates/.placeholder | 0 dashboard/doc/source/_theme/layout.html | 83 + dashboard/doc/source/_theme/theme.conf | 4 + dashboard/doc/source/conf.py | 427 ++ dashboard/doc/source/contributing.rst | 204 + dashboard/doc/source/faq.rst | 37 + dashboard/doc/source/glossary.rst | 24 + dashboard/doc/source/index.rst | 126 + dashboard/doc/source/intro.rst | 124 + dashboard/doc/source/quickstart.rst | 207 + .../doc/source/ref/context_processors.rst | 6 + dashboard/doc/source/ref/decorators.rst | 6 + dashboard/doc/source/ref/exceptions.rst | 6 + dashboard/doc/source/ref/forms.rst | 98 + dashboard/doc/source/ref/horizon.rst | 45 + dashboard/doc/source/ref/middleware.rst | 6 + dashboard/doc/source/ref/run_tests.rst | 224 + dashboard/doc/source/ref/tables.rst | 82 + dashboard/doc/source/ref/tabs.rst | 45 + dashboard/doc/source/ref/test.rst | 25 + dashboard/doc/source/ref/workflows.rst | 33 + dashboard/doc/source/releases/2012_1.rst | 148 + dashboard/doc/source/releases/2012_2.rst | 159 + dashboard/doc/source/testing.rst | 41 + dashboard/doc/source/topics/customizing.rst | 139 + dashboard/doc/source/topics/deployment.rst | 190 + dashboard/doc/source/topics/tables.rst | 129 + dashboard/doc/source/topics/testing.rst | 276 + dashboard/doc/source/topics/tutorial.rst | 545 ++ dashboard/horizon/__init__.py | 49 + dashboard/horizon/base.py | 788 +++ dashboard/horizon/browsers/__init__.py | 18 + dashboard/horizon/browsers/base.py | 150 + dashboard/horizon/browsers/breadcrumb.py | 48 + dashboard/horizon/browsers/views.py | 49 + dashboard/horizon/conf/__init__.py | 34 + .../horizon/conf/dash_template/__init__.py | 0 .../conf/dash_template/dashboard.py.tmpl | 13 + .../horizon/conf/dash_template/models.py | 3 + .../static/dash_name/css/dash_name.css | 1 + .../static/dash_name/js/dash_name.js | 1 + .../templates/dash_name/base.html | 11 + dashboard/horizon/conf/default.py | 35 + .../horizon/conf/panel_template/__init__.py | 0 .../horizon/conf/panel_template/models.py | 3 + .../horizon/conf/panel_template/panel.py.tmpl | 13 + .../templates/panel_name/index.html | 12 + .../horizon/conf/panel_template/tests.py.tmpl | 7 + dashboard/horizon/conf/panel_template/urls.py | 8 + .../horizon/conf/panel_template/views.py | 10 + dashboard/horizon/context_processors.py | 44 + dashboard/horizon/decorators.py | 94 + dashboard/horizon/exceptions.py | 314 + dashboard/horizon/forms/__init__.py | 24 + dashboard/horizon/forms/base.py | 60 + dashboard/horizon/forms/fields.py | 70 + dashboard/horizon/forms/views.py | 115 + dashboard/horizon/loaders.py | 48 + .../locale/bg_BG/LC_MESSAGES/django.mo | Bin 0 -> 2950 bytes .../locale/bg_BG/LC_MESSAGES/django.po | 521 ++ .../locale/bg_BG/LC_MESSAGES/djangojs.mo | Bin 0 -> 378 bytes .../locale/bg_BG/LC_MESSAGES/djangojs.po | 73 + .../horizon/locale/en/LC_MESSAGES/django.mo | Bin 0 -> 4746 bytes .../horizon/locale/en/LC_MESSAGES/django.po | 523 ++ .../horizon/locale/en/LC_MESSAGES/djangojs.mo | Bin 0 -> 378 bytes .../horizon/locale/en/LC_MESSAGES/djangojs.po | 73 + .../horizon/locale/es/LC_MESSAGES/django.mo | Bin 0 -> 5039 bytes .../horizon/locale/es/LC_MESSAGES/django.po | 524 ++ .../horizon/locale/es/LC_MESSAGES/djangojs.mo | Bin 0 -> 420 bytes .../horizon/locale/es/LC_MESSAGES/djangojs.po | 74 + .../horizon/locale/fr/LC_MESSAGES/django.mo | Bin 0 -> 414 bytes .../horizon/locale/fr/LC_MESSAGES/django.po | 510 ++ .../horizon/locale/fr/LC_MESSAGES/djangojs.mo | Bin 0 -> 419 bytes .../horizon/locale/fr/LC_MESSAGES/djangojs.po | 74 + .../horizon/locale/it/LC_MESSAGES/django.mo | Bin 0 -> 2838 bytes .../horizon/locale/it/LC_MESSAGES/django.po | 520 ++ .../horizon/locale/it/LC_MESSAGES/djangojs.mo | Bin 0 -> 420 bytes .../horizon/locale/it/LC_MESSAGES/djangojs.po | 74 + .../horizon/locale/ja/LC_MESSAGES/django.mo | Bin 0 -> 6233 bytes .../horizon/locale/ja/LC_MESSAGES/django.po | 518 ++ .../horizon/locale/ja/LC_MESSAGES/djangojs.mo | Bin 0 -> 413 bytes .../horizon/locale/ja/LC_MESSAGES/djangojs.po | 73 + .../locale/ko_KR/LC_MESSAGES/django.mo | Bin 0 -> 731 bytes .../locale/ko_KR/LC_MESSAGES/django.po | 506 ++ .../locale/ko_KR/LC_MESSAGES/djangojs.mo | Bin 0 -> 378 bytes .../locale/ko_KR/LC_MESSAGES/djangojs.po | 73 + .../locale/nl_NL/LC_MESSAGES/django.mo | Bin 0 -> 2887 bytes .../locale/nl_NL/LC_MESSAGES/django.po | 519 ++ .../locale/nl_NL/LC_MESSAGES/djangojs.mo | Bin 0 -> 378 bytes .../locale/nl_NL/LC_MESSAGES/djangojs.po | 73 + .../horizon/locale/pl/LC_MESSAGES/django.mo | Bin 0 -> 572 bytes .../horizon/locale/pl/LC_MESSAGES/django.po | 530 ++ .../horizon/locale/pl/LC_MESSAGES/djangojs.mo | Bin 0 -> 478 bytes .../horizon/locale/pl/LC_MESSAGES/djangojs.po | 76 + .../horizon/locale/pt/LC_MESSAGES/django.mo | Bin 0 -> 4355 bytes .../horizon/locale/pt/LC_MESSAGES/django.po | 521 ++ .../horizon/locale/pt/LC_MESSAGES/djangojs.mo | Bin 0 -> 420 bytes .../horizon/locale/pt/LC_MESSAGES/djangojs.po | 74 + .../locale/pt_BR/LC_MESSAGES/django.mo | Bin 0 -> 4507 bytes .../locale/pt_BR/LC_MESSAGES/django.po | 525 ++ .../locale/pt_BR/LC_MESSAGES/djangojs.mo | Bin 0 -> 419 bytes .../locale/pt_BR/LC_MESSAGES/djangojs.po | 74 + .../horizon/locale/ru/LC_MESSAGES/django.mo | Bin 0 -> 585 bytes .../horizon/locale/ru/LC_MESSAGES/django.po | 517 ++ .../horizon/locale/ru/LC_MESSAGES/djangojs.mo | Bin 0 -> 494 bytes .../horizon/locale/ru/LC_MESSAGES/djangojs.po | 75 + .../locale/zh_CN/LC_MESSAGES/django.mo | Bin 0 -> 875 bytes .../locale/zh_CN/LC_MESSAGES/django.po | 511 ++ .../locale/zh_CN/LC_MESSAGES/djangojs.mo | Bin 0 -> 413 bytes .../locale/zh_CN/LC_MESSAGES/djangojs.po | 73 + .../locale/zh_TW/LC_MESSAGES/django.mo | Bin 0 -> 5020 bytes .../locale/zh_TW/LC_MESSAGES/django.po | 517 ++ .../locale/zh_TW/LC_MESSAGES/djangojs.mo | Bin 0 -> 413 bytes .../locale/zh_TW/LC_MESSAGES/djangojs.po | 73 + dashboard/horizon/management/__init__.py | 0 .../horizon/management/commands/__init__.py | 0 .../horizon/management/commands/startdash.py | 59 + .../horizon/management/commands/startpanel.py | 97 + dashboard/horizon/messages.py | 83 + dashboard/horizon/middleware.py | 113 + dashboard/horizon/models.py | 23 + dashboard/horizon/site_urls.py | 45 + .../horizon/static/bootstrap/js/bootstrap.js | 1720 ++++++ .../static/bootstrap/js/bootstrap.min.js | 1 + .../horizon/js/horizon.communication.js | 51 + .../horizon/static/horizon/js/horizon.conf.js | 31 + .../static/horizon/js/horizon.cookies.js | 23 + .../static/horizon/js/horizon.forms.js | 134 + .../static/horizon/js/horizon.instances.js | 109 + .../horizon/static/horizon/js/horizon.js | 35 + .../static/horizon/js/horizon.messages.js | 46 + .../static/horizon/js/horizon.modals.js | 223 + .../horizon/js/horizon.networktopology.js | 277 + .../static/horizon/js/horizon.projects.js | 590 ++ .../static/horizon/js/horizon.quota.js | 246 + .../static/horizon/js/horizon.tables.js | 393 ++ .../horizon/static/horizon/js/horizon.tabs.js | 84 + .../static/horizon/js/horizon.templates.js | 17 + .../static/horizon/js/horizon.users.js | 31 + .../static/horizon/js/horizon.utils.js | 44 + .../horizon/static/horizon/lib/hogan-2.0.0.js | 576 ++ .../lib/jquery/jquery-ui-1.9.2.custom.min.js | 6 + .../horizon/lib/jquery/jquery.cookie.js | 47 + .../static/horizon/lib/jquery/jquery.min.js | 4 + .../horizon/lib/jquery/jquery.quicksearch.js | 150 + .../horizon/lib/jquery/jquery.table-sorter.js | 1031 ++++ dashboard/horizon/static/horizon/lib/json2.js | 487 ++ .../static/horizon/lib/qunit/qunit.css | 238 + .../horizon/static/horizon/lib/qunit/qunit.js | 1865 ++++++ .../horizon/static/horizon/lib/spin.jquery.js | 17 + dashboard/horizon/static/horizon/lib/spin.js | 2 + .../horizon/lib/underscore/underscore-min.js | 32 + .../horizon/static/horizon/tests/messages.js | 38 + .../horizon/static/horizon/tests/modals.js | 22 + .../horizon/static/horizon/tests/tables.js | 49 + .../horizon/static/horizon/tests/templates.js | 7 + dashboard/horizon/tables/__init__.py | 22 + dashboard/horizon/tables/actions.py | 623 ++ dashboard/horizon/tables/base.py | 1335 +++++ dashboard/horizon/tables/views.py | 261 + dashboard/horizon/tabs/__init__.py | 18 + dashboard/horizon/tabs/base.py | 459 ++ dashboard/horizon/tabs/views.py | 141 + dashboard/horizon/templates/_header.html | 8 + dashboard/horizon/templates/_stylesheets.html | 1 + dashboard/horizon/templates/auth/_login.html | 28 + dashboard/horizon/templates/auth/login.html | 10 + dashboard/horizon/templates/base.html | 37 + .../horizon/templates/horizon/_conf.html | 16 + .../horizon/templates/horizon/_messages.html | 29 + .../horizon/templates/horizon/_nav_list.html | 13 + .../horizon/templates/horizon/_scripts.html | 45 + .../templates/horizon/_subnav_list.html | 16 + .../horizon/client_side/_alert_message.html | 21 + .../horizon/client_side/_loading.html | 12 + .../templates/horizon/client_side/_modal.html | 22 + .../horizon/client_side/_project_user.html | 26 + .../horizon/client_side/_script_loader.html | 20 + .../horizon/client_side/_table_row.html | 10 + .../horizon/client_side/template.html | 1 + .../horizon/client_side/templates.html | 5 + .../templates/horizon/common/_breadcrumb.html | 20 + .../templates/horizon/common/_data_table.html | 78 + .../horizon/common/_data_table_row.html | 3 + .../common/_data_table_row_action.html | 5 + .../common/_data_table_row_actions.html | 30 + .../common/_data_table_table_actions.html | 23 + .../horizon/common/_detail_table.html | 1 + .../horizon/common/_form_fields.html | 22 + .../templates/horizon/common/_modal.html | 10 + .../templates/horizon/common/_modal_form.html | 22 + .../horizon/common/_page_header.html | 6 + .../horizon/common/_progress_bar.html | 4 + .../horizon/common/_quota_summary.html | 21 + .../horizon/common/_region_selector.html | 15 + .../horizon/common/_resource_browser.html | 13 + .../templates/horizon/common/_sidebar.html | 35 + .../horizon/common/_sidebar_module.html | 10 + .../templates/horizon/common/_tab_group.html | 23 + .../horizon/common/_usage_summary.html | 19 + .../templates/horizon/common/_workflow.html | 43 + .../horizon/common/_workflow_step.html | 13 + .../common/_workflow_step_update_members.html | 39 + .../horizon/templates/horizon/qunit.html | 73 + dashboard/horizon/templates/splash.html | 17 + dashboard/horizon/templatetags/__init__.py | 0 dashboard/horizon/templatetags/branding.py | 63 + dashboard/horizon/templatetags/horizon.py | 145 + dashboard/horizon/templatetags/parse_date.py | 54 + dashboard/horizon/templatetags/sizeformat.py | 77 + .../horizon/templatetags/truncate_filter.py | 35 + dashboard/horizon/test/__init__.py | 0 dashboard/horizon/test/helpers.py | 170 + dashboard/horizon/test/settings.py | 177 + dashboard/horizon/test/templates/404.html | 0 dashboard/horizon/test/templates/_tab.html | 1 + .../horizon/test/templates/base-sidebar.html | 0 .../test/templates/registration/login.html | 0 .../horizon/test/templates/tab_group.html | 1 + .../horizon/test/templates/workflow.html | 1 + .../horizon/test/test_dashboards/__init__.py | 0 .../test/test_dashboards/cats/__init__.py | 0 .../test/test_dashboards/cats/dashboard.py | 25 + .../test_dashboards/cats/kittens/__init__.py | 0 .../test_dashboards/cats/kittens/models.py | 3 + .../test_dashboards/cats/kittens/panel.py | 14 + .../cats/kittens/templates/kittens/index.html | 12 + .../test/test_dashboards/cats/kittens/urls.py | 7 + .../test_dashboards/cats/kittens/views.py | 10 + .../test/test_dashboards/cats/models.py | 3 + .../cats/static/cats/css/cats.css | 1 + .../cats/static/cats/js/cats.js | 1 + .../cats/templates/cats/base.html | 11 + .../test_dashboards/cats/tigers/__init__.py | 0 .../test_dashboards/cats/tigers/models.py | 3 + .../test/test_dashboards/cats/tigers/panel.py | 14 + .../cats/tigers/templates/tigers/index.html | 12 + .../test/test_dashboards/cats/tigers/urls.py | 7 + .../test/test_dashboards/cats/tigers/views.py | 10 + .../test/test_dashboards/dogs/__init__.py | 0 .../test/test_dashboards/dogs/dashboard.py | 13 + .../test/test_dashboards/dogs/models.py | 3 + .../test_dashboards/dogs/puppies/__init__.py | 0 .../test_dashboards/dogs/puppies/models.py | 3 + .../test_dashboards/dogs/puppies/panel.py | 13 + .../dogs/puppies/templates/puppies/index.html | 12 + .../test/test_dashboards/dogs/puppies/urls.py | 7 + .../test_dashboards/dogs/puppies/views.py | 10 + .../dogs/static/dogs/css/dogs.css | 1 + .../dogs/static/dogs/js/dogs.js | 1 + .../dogs/templates/dogs/base.html | 11 + dashboard/horizon/test/tests/__init__.py | 0 dashboard/horizon/test/tests/base.py | 294 + dashboard/horizon/test/tests/messages.py | 56 + dashboard/horizon/test/tests/middleware.py | 34 + dashboard/horizon/test/tests/selenium.py | 31 + dashboard/horizon/test/tests/tables.py | 755 +++ dashboard/horizon/test/tests/tabs.py | 306 + dashboard/horizon/test/tests/templatetags.py | 49 + dashboard/horizon/test/tests/utils.py | 196 + dashboard/horizon/test/tests/workflows.py | 266 + dashboard/horizon/test/urls.py | 43 + dashboard/horizon/utils/__init__.py | 0 dashboard/horizon/utils/fields.py | 128 + dashboard/horizon/utils/filters.py | 19 + dashboard/horizon/utils/functions.py | 9 + dashboard/horizon/utils/html.py | 56 + dashboard/horizon/utils/memoized.py | 50 + dashboard/horizon/utils/secret_key.py | 67 + dashboard/horizon/utils/validators.py | 32 + dashboard/horizon/version.py | 19 + dashboard/horizon/views.py | 53 + dashboard/horizon/workflows/__init__.py | 2 + dashboard/horizon/workflows/base.py | 845 +++ dashboard/horizon/workflows/views.py | 158 + dashboard/manage.py | 11 + dashboard/openstack-common.conf | 7 + dashboard/openstack_dashboard/__init__.py | 0 dashboard/openstack_dashboard/api/__init__.py | 44 + dashboard/openstack_dashboard/api/base.py | 175 + dashboard/openstack_dashboard/api/cinder.py | 146 + dashboard/openstack_dashboard/api/glance.py | 110 + dashboard/openstack_dashboard/api/keystone.py | 302 + dashboard/openstack_dashboard/api/lbaas.py | 291 + dashboard/openstack_dashboard/api/network.py | 172 + dashboard/openstack_dashboard/api/nova.py | 555 ++ dashboard/openstack_dashboard/api/quantum.py | 433 ++ dashboard/openstack_dashboard/api/swift.py | 231 + .../{ => openstack_dashboard}/api/windc.py | 0 .../openstack_dashboard/context_processors.py | 63 + .../dashboards/__init__.py | 0 .../dashboards/admin/__init__.py | 0 .../dashboards/admin/dashboard.py | 37 + .../dashboards/admin/flavors/__init__.py | 0 .../admin/flavors/extras/__init__.py | 0 .../dashboards/admin/flavors/extras/forms.py | 66 + .../dashboards/admin/flavors/extras/tables.py | 74 + .../dashboards/admin/flavors/extras/tests.py | 62 + .../dashboards/admin/flavors/extras/urls.py | 29 + .../dashboards/admin/flavors/extras/views.py | 93 + .../dashboards/admin/flavors/forms.py | 131 + .../dashboards/admin/flavors/panel.py | 33 + .../dashboards/admin/flavors/tables.py | 68 + .../flavors/templates/flavors/_create.html | 25 + .../flavors/templates/flavors/_edit.html | 26 + .../flavors/templates/flavors/create.html | 11 + .../admin/flavors/templates/flavors/edit.html | 11 + .../templates/flavors/extras/_create.html | 27 + .../templates/flavors/extras/_edit.html | 27 + .../templates/flavors/extras/_index.html | 14 + .../templates/flavors/extras/create.html | 12 + .../templates/flavors/extras/edit.html | 12 + .../templates/flavors/extras/index.html | 12 + .../flavors/templates/flavors/index.html | 11 + .../dashboards/admin/flavors/tests.py | 196 + .../dashboards/admin/flavors/urls.py | 30 + .../dashboards/admin/flavors/views.py | 83 + .../dashboards/admin/images/__init__.py | 0 .../dashboards/admin/images/forms.py | 30 + .../dashboards/admin/images/panel.py | 33 + .../dashboards/admin/images/tables.py | 51 + .../images/templates/images/_create.html | 34 + .../images/templates/images/_update.html | 25 + .../admin/images/templates/images/create.html | 11 + .../admin/images/templates/images/index.html | 11 + .../admin/images/templates/images/update.html | 12 + .../dashboards/admin/images/tests.py | 104 + .../dashboards/admin/images/urls.py | 31 + .../dashboards/admin/images/views.py | 75 + .../dashboards/admin/info/__init__.py | 0 .../dashboards/admin/info/panel.py | 33 + .../dashboards/admin/info/tables.py | 79 + .../dashboards/admin/info/tabs.py | 66 + .../admin/info/templates/info/index.html | 15 + .../dashboards/admin/info/tests.py | 76 + .../dashboards/admin/info/urls.py | 27 + .../dashboards/admin/info/views.py | 33 + .../dashboards/admin/instances/__init__.py | 0 .../dashboards/admin/instances/panel.py | 34 + .../dashboards/admin/instances/tables.py | 122 + .../instances/templates/instances/index.html | 11 + .../dashboards/admin/instances/tests.py | 176 + .../dashboards/admin/instances/urls.py | 36 + .../dashboards/admin/instances/views.py | 90 + .../dashboards/admin/models.py | 23 + .../dashboards/admin/networks/__init__.py | 0 .../dashboards/admin/networks/forms.py | 101 + .../dashboards/admin/networks/panel.py | 29 + .../admin/networks/ports/__init__.py | 0 .../dashboards/admin/networks/ports/forms.py | 103 + .../dashboards/admin/networks/ports/tables.py | 85 + .../dashboards/admin/networks/ports/tabs.py | 49 + .../dashboards/admin/networks/ports/urls.py | 28 + .../dashboards/admin/networks/ports/views.py | 72 + .../admin/networks/subnets/__init__.py | 0 .../admin/networks/subnets/tables.py | 83 + .../dashboards/admin/networks/subnets/urls.py | 29 + .../admin/networks/subnets/views.py | 42 + .../admin/networks/subnets/workflows.py | 60 + .../dashboards/admin/networks/tables.py | 82 + .../networks/templates/networks/_create.html | 25 + .../networks/templates/networks/_update.html | 24 + .../networks/templates/networks/create.html | 11 + .../networks/templates/networks/index.html | 21 + .../templates/networks/ports/_create.html | 25 + .../templates/networks/ports/_update.html | 29 + .../templates/networks/ports/create.html | 11 + .../templates/networks/ports/update.html | 11 + .../templates/networks/subnets/create.html | 11 + .../templates/networks/subnets/index.html | 11 + .../templates/networks/subnets/update.html | 11 + .../networks/templates/networks/update.html | 11 + .../dashboards/admin/networks/tests.py | 822 +++ .../dashboards/admin/networks/urls.py | 47 + .../dashboards/admin/networks/views.py | 142 + .../dashboards/admin/overview/__init__.py | 0 .../dashboards/admin/overview/panel.py | 34 + .../overview/templates/overview/usage.csv | 10 + .../overview/templates/overview/usage.html | 22 + .../dashboards/admin/overview/tests.py | 98 + .../dashboards/admin/overview/urls.py | 29 + .../dashboards/admin/overview/views.py | 48 + .../dashboards/admin/projects/__init__.py | 0 .../dashboards/admin/projects/forms.py | 33 + .../dashboards/admin/projects/panel.py | 33 + .../dashboards/admin/projects/tables.py | 166 + .../templates/projects/_add_user.html | 25 + .../projects/templates/projects/_create.html | 25 + .../templates/projects/_create_user.html | 25 + .../projects/templates/projects/_quotas.html | 24 + .../projects/templates/projects/_update.html | 25 + .../templates/projects/_update_members.html | 39 + .../projects/templates/projects/add_user.html | 11 + .../projects/templates/projects/create.html | 12 + .../templates/projects/create_user.html | 12 + .../projects/templates/projects/index.html | 11 + .../projects/templates/projects/quotas.html | 11 + .../projects/templates/projects/update.html | 12 + .../projects/templates/projects/usage.html | 14 + .../projects/templates/projects/users.html | 18 + .../dashboards/admin/projects/tests.py | 913 +++ .../dashboards/admin/projects/urls.py | 37 + .../dashboards/admin/projects/views.py | 198 + .../dashboards/admin/projects/workflows.py | 409 ++ .../dashboards/admin/routers/__init__.py | 0 .../dashboards/admin/routers/panel.py | 29 + .../admin/routers/ports/__init__.py | 0 .../dashboards/admin/routers/ports/tables.py | 47 + .../dashboards/admin/routers/ports/tabs.py | 31 + .../dashboards/admin/routers/ports/urls.py | 24 + .../dashboards/admin/routers/ports/views.py | 28 + .../dashboards/admin/routers/tables.py | 75 + .../dashboards/admin/routers/tabs.py | 28 + .../templates/routers/_detail_overview.html | 22 + .../routers/templates/routers/detail.html | 15 + .../routers/templates/routers/index.html | 11 + .../dashboards/admin/routers/tests.py | 62 + .../dashboards/admin/routers/urls.py | 27 + .../dashboards/admin/routers/views.py | 71 + .../dashboards/admin/users/__init__.py | 0 .../dashboards/admin/users/forms.py | 196 + .../dashboards/admin/users/panel.py | 33 + .../dashboards/admin/users/tables.py | 122 + .../admin/users/templates/users/_create.html | 34 + .../admin/users/templates/users/_update.html | 34 + .../admin/users/templates/users/create.html | 12 + .../admin/users/templates/users/index.html | 11 + .../admin/users/templates/users/update.html | 12 + .../dashboards/admin/users/tests.py | 423 ++ .../dashboards/admin/users/urls.py | 28 + .../dashboards/admin/users/views.py | 112 + .../dashboards/admin/volumes/__init__.py | 0 .../dashboards/admin/volumes/forms.py | 44 + .../dashboards/admin/volumes/panel.py | 14 + .../dashboards/admin/volumes/tables.py | 56 + .../volumes/_create_volume_type.html | 29 + .../templates/volumes/create_volume_type.html | 11 + .../volumes/templates/volumes/detail.html | 15 + .../volumes/templates/volumes/index.html | 17 + .../dashboards/admin/volumes/tests.py | 92 + .../dashboards/admin/volumes/urls.py | 9 + .../dashboards/admin/volumes/views.py | 82 + .../dashboards/project/__init__.py | 0 .../project/access_and_security/__init__.py | 0 .../api_access/__init__.py | 0 .../access_and_security/api_access/tables.py | 63 + .../access_and_security/api_access/tests.py | 51 + .../access_and_security/api_access/urls.py | 29 + .../access_and_security/api_access/views.py | 135 + .../floating_ips/__init__.py | 0 .../access_and_security/floating_ips/forms.py | 48 + .../floating_ips/tables.py | 133 + .../access_and_security/floating_ips/tests.py | 201 + .../access_and_security/floating_ips/urls.py | 29 + .../access_and_security/floating_ips/utils.py | 31 + .../access_and_security/floating_ips/views.py | 70 + .../floating_ips/workflows.py | 144 + .../access_and_security/keypairs/__init__.py | 0 .../access_and_security/keypairs/forms.py | 66 + .../access_and_security/keypairs/tables.py | 62 + .../access_and_security/keypairs/tests.py | 144 + .../access_and_security/keypairs/urls.py | 33 + .../access_and_security/keypairs/views.py | 82 + .../project/access_and_security/panel.py | 30 + .../security_groups/__init__.py | 0 .../security_groups/forms.py | 249 + .../security_groups/tables.py | 121 + .../security_groups/tests.py | 373 ++ .../security_groups/urls.py | 34 + .../security_groups/views.py | 101 + .../project/access_and_security/tabs.py | 126 + .../api_access/ec2rc.sh.template | 15 + .../api_access/openrc.sh.template | 24 + .../floating_ips/_allocate.html | 33 + .../floating_ips/allocate.html | 7 + .../floating_ips/associate.html | 11 + .../templates/access_and_security/index.html | 15 + .../access_and_security/keypairs/_create.html | 25 + .../access_and_security/keypairs/_import.html | 25 + .../access_and_security/keypairs/create.html | 12 + .../keypairs/download.html | 21 + .../access_and_security/keypairs/import.html | 12 + .../security_groups/_add_rule.html | 28 + .../security_groups/_create.html | 25 + .../security_groups/add_rule.html | 12 + .../security_groups/create.html | 11 + .../security_groups/detail.html | 11 + .../project/access_and_security/tests.py | 101 + .../project/access_and_security/urls.py | 37 + .../project/access_and_security/views.py | 33 + .../dashboards/project/containers/__init__.py | 0 .../dashboards/project/containers/browsers.py | 33 + .../dashboards/project/containers/forms.py | 152 + .../dashboards/project/containers/panel.py | 33 + .../dashboards/project/containers/tables.py | 241 + .../templates/containers/_copy.html | 24 + .../templates/containers/_create.html | 24 + .../templates/containers/_upload.html | 26 + .../containers/templates/containers/copy.html | 13 + .../templates/containers/create.html | 11 + .../templates/containers/index.html | 13 + .../templates/containers/upload.html | 15 + .../dashboards/project/containers/tests.py | 232 + .../dashboards/project/containers/urls.py | 50 + .../dashboards/project/containers/views.py | 221 + .../dashboards/project/dashboard.py | 51 + .../project/images_and_snapshots/__init__.py | 0 .../images_and_snapshots/images/__init__.py | 0 .../images_and_snapshots/images/forms.py | 191 + .../images_and_snapshots/images/tables.py | 196 + .../images_and_snapshots/images/tabs.py | 45 + .../images_and_snapshots/images/tests.py | 232 + .../images_and_snapshots/images/urls.py | 34 + .../images_and_snapshots/images/views.py | 84 + .../project/images_and_snapshots/panel.py | 30 + .../snapshots/__init__.py | 0 .../images_and_snapshots/snapshots/forms.py | 57 + .../images_and_snapshots/snapshots/tables.py | 60 + .../images_and_snapshots/snapshots/tests.py | 98 + .../images_and_snapshots/snapshots/urls.py | 30 + .../images_and_snapshots/snapshots/views.py | 63 + .../images_and_snapshots/images/_create.html | 34 + .../images/_detail_overview.html | 68 + .../images_and_snapshots/images/_update.html | 24 + .../images_and_snapshots/images/create.html | 11 + .../images_and_snapshots/images/detail.html | 16 + .../images_and_snapshots/images/update.html | 11 + .../templates/images_and_snapshots/index.html | 19 + .../snapshots/_create.html | 25 + .../snapshots/_detail_overview.html | 42 + .../snapshots/create.html | 11 + .../snapshots/detail.html | 15 + .../project/images_and_snapshots/tests.py | 175 + .../project/images_and_snapshots/urls.py | 35 + .../project/images_and_snapshots/views.py | 93 + .../volume_snapshots/__init__.py | 0 .../volume_snapshots/tables.py | 94 + .../volume_snapshots/tabs.py | 49 + .../volume_snapshots/tests.py | 92 + .../dashboards/project/instances/__init__.py | 0 .../dashboards/project/instances/panel.py | 29 + .../dashboards/project/instances/tables.py | 479 ++ .../dashboards/project/instances/tabs.py | 85 + .../templates/instances/_detail_console.html | 21 + .../templates/instances/_detail_log.html | 17 + .../templates/instances/_detail_overview.html | 102 + .../templates/instances/_instance_ips.html | 10 + .../instances/_launch_customize_help.html | 3 + .../instances/_launch_details_help.html | 53 + .../instances/_launch_network_help.html | 3 + .../instances/_launch_volumes_help.html | 3 + .../templates/instances/_update_networks.html | 35 + .../instances/templates/instances/detail.html | 15 + .../instances/templates/instances/index.html | 11 + .../instances/templates/instances/launch.html | 11 + .../instances/templates/instances/update.html | 11 + .../dashboards/project/instances/tests.py | 1273 +++++ .../dashboards/project/instances/urls.py | 38 + .../dashboards/project/instances/views.py | 198 + .../project/instances/workflows/__init__.py | 2 + .../instances/workflows/create_instance.py | 522 ++ .../instances/workflows/update_instance.py | 178 + .../project/loadbalancers/__init__.py | 0 .../project/loadbalancers/models.py | 3 + .../dashboards/project/loadbalancers/panel.py | 16 + .../project/loadbalancers/tables.py | 162 + .../dashboards/project/loadbalancers/tabs.py | 170 + .../loadbalancers/_member_details.html | 30 + .../templates/loadbalancers/_members_tab.html | 5 + .../loadbalancers/_monitor_details.html | 39 + .../loadbalancers/_monitors_tab.html | 5 + .../loadbalancers/_pool_details.html | 42 + .../templates/loadbalancers/_pools_tab.html | 5 + .../templates/loadbalancers/_vip_details.html | 48 + .../templates/loadbalancers/addmember.html | 11 + .../templates/loadbalancers/addmonitor.html | 11 + .../templates/loadbalancers/addpool.html | 11 + .../templates/loadbalancers/addvip.html | 11 + .../templates/loadbalancers/details_tabs.html | 15 + .../dashboards/project/loadbalancers/tests.py | 299 + .../dashboards/project/loadbalancers/urls.py | 38 + .../dashboards/project/loadbalancers/views.py | 152 + .../project/loadbalancers/workflows.py | 438 ++ .../dashboards/project/models.py | 23 + .../project/network_topology/__init__.py | 19 + .../project/network_topology/panel.py | 34 + .../templates/network_topology/index.html | 36 + .../project/network_topology/urls.py | 31 + .../project/network_topology/views.py | 99 + .../dashboards/project/networks/__init__.py | 0 .../dashboards/project/networks/forms.py | 59 + .../dashboards/project/networks/panel.py | 29 + .../project/networks/ports/__init__.py | 0 .../project/networks/ports/forms.py | 56 + .../project/networks/ports/tables.py | 71 + .../dashboards/project/networks/ports/tabs.py | 49 + .../dashboards/project/networks/ports/urls.py | 28 + .../project/networks/ports/views.py | 72 + .../project/networks/subnets/__init__.py | 0 .../project/networks/subnets/tables.py | 106 + .../project/networks/subnets/tabs.py | 49 + .../project/networks/subnets/urls.py | 28 + .../project/networks/subnets/views.py | 103 + .../project/networks/subnets/workflows.py | 198 + .../dashboards/project/networks/tables.py | 108 + .../networks/templates/networks/_create.html | 24 + .../templates/networks/_detail_overview.html | 28 + .../templates/networks/_network_ips.html | 10 + .../networks/templates/networks/_update.html | 24 + .../networks/templates/networks/create.html | 11 + .../networks/templates/networks/detail.html | 18 + .../networks/templates/networks/index.html | 11 + .../networks/ports/_detail_overview.html | 43 + .../templates/networks/ports/_port_ips.html | 7 + .../templates/networks/ports/_update.html | 29 + .../templates/networks/ports/detail.html | 15 + .../templates/networks/ports/update.html | 11 + .../networks/subnets/_detail_overview.html | 49 + .../templates/networks/subnets/create.html | 11 + .../templates/networks/subnets/detail.html | 15 + .../templates/networks/subnets/index.html | 11 + .../templates/networks/subnets/update.html | 11 + .../networks/templates/networks/update.html | 11 + .../dashboards/project/networks/tests.py | 1356 +++++ .../dashboards/project/networks/urls.py | 42 + .../dashboards/project/networks/views.py | 144 + .../dashboards/project/networks/workflows.py | 370 ++ .../dashboards/project/overview/__init__.py | 0 .../dashboards/project/overview/panel.py | 33 + .../overview/templates/overview/usage.csv | 11 + .../overview/templates/overview/usage.html | 13 + .../dashboards/project/overview/tests.py | 145 + .../dashboards/project/overview/urls.py | 30 + .../dashboards/project/overview/views.py | 37 + .../dashboards/project/routers/__init__.py | 0 .../dashboards/project/routers/forms.py | 41 + .../dashboards/project/routers/panel.py | 29 + .../project/routers/ports/__init__.py | 0 .../dashboards/project/routers/ports/forms.py | 137 + .../project/routers/ports/tables.py | 93 + .../dashboards/project/routers/ports/tabs.py | 47 + .../dashboards/project/routers/ports/urls.py | 24 + .../dashboards/project/routers/ports/views.py | 99 + .../dashboards/project/routers/tables.py | 141 + .../dashboards/project/routers/tabs.py | 44 + .../routers/templates/routers/_create.html | 21 + .../templates/routers/_detail_overview.html | 20 + .../routers/templates/routers/create.html | 11 + .../routers/templates/routers/detail.html | 15 + .../routers/templates/routers/index.html | 11 + .../templates/routers/ports/_create.html | 25 + .../templates/routers/ports/_setgateway.html | 25 + .../templates/routers/ports/create.html | 11 + .../templates/routers/ports/setgateway.html | 11 + .../dashboards/project/routers/tests.py | 241 + .../dashboards/project/routers/urls.py | 34 + .../dashboards/project/routers/views.py | 147 + .../dashboards/project/volumes/__init__.py | 0 .../dashboards/project/volumes/forms.py | 266 + .../dashboards/project/volumes/panel.py | 30 + .../dashboards/project/volumes/tables.py | 240 + .../dashboards/project/volumes/tabs.py | 48 + .../volumes/templates/volumes/_attach.html | 25 + .../volumes/templates/volumes/_create.html | 57 + .../templates/volumes/_create_snapshot.html | 25 + .../templates/volumes/_detail_overview.html | 60 + .../volumes/templates/volumes/attach.html | 11 + .../volumes/templates/volumes/create.html | 11 + .../templates/volumes/create_snapshot.html | 11 + .../volumes/templates/volumes/detail.html | 15 + .../volumes/templates/volumes/index.html | 11 + .../dashboards/project/volumes/tests.py | 474 ++ .../dashboards/project/volumes/urls.py | 35 + .../dashboards/project/volumes/views.py | 187 + .../dashboards/project/windc/.idea/.name | 1 + .../project/windc/.idea/encodings.xml | 5 + .../dashboards/project/windc/.idea/misc.xml | 5 + .../project/windc/.idea/modules.xml | 9 + .../dashboards/project/windc/.idea/other.xml | 7 + .../windc/.idea/scopes/scope_settings.xml | 5 + .../project/windc/.idea/testrunner.xml | 8 + .../dashboards/project/windc/.idea/vcs.xml | 7 + .../dashboards/project/windc/.idea/windc.iml | 9 + .../project/windc/.idea/workspace.xml | 320 ++ .../dashboards/project/windc/__init__.py | 0 .../dashboards/project}/windc/forms.py | 0 .../dashboards/project}/windc/panel.py | 0 .../dashboards/project}/windc/tables.py | 0 .../dashboards/project}/windc/tabs.py | 0 .../templates/windc/_data_center_help.html | 0 .../windc/templates/windc/_dc_help.html | 0 .../windc/templates/windc/_iis_help.html | 0 .../windc/templates/windc/_services.html | 0 .../windc/templates/windc/_services_tabs.html | 0 .../windc/templates/windc/create.html | 0 .../windc/templates/windc/create_dc.html | 0 .../project}/windc/templates/windc/index.html | 0 .../windc/templates/windc/services.html | 0 .../windc/templates/windc/update.html | 0 .../dashboards/project}/windc/urls.py | 0 .../dashboards/project}/windc/views.py | 0 .../dashboards/project}/windc/workflows.py | 0 .../dashboards/settings/__init__.py | 0 .../dashboards/settings/dashboard.py | 31 + .../dashboards/settings/models.py | 23 + .../dashboards/settings/user/__init__.py | 0 .../dashboards/settings/user/forms.py | 75 + .../dashboards/settings/user/panel.py | 29 + .../user/templates/user/_settings.html | 26 + .../user/templates/user/settings.html | 11 + .../dashboards/settings/user/tests.py | 32 + .../dashboards/settings/user/urls.py | 23 + .../dashboards/settings/user/views.py | 31 + dashboard/openstack_dashboard/exceptions.py | 61 + .../openstack_dashboard/local/__init__.py | 0 .../local/local_settings.py.example | 190 + .../locale/bg_BG/LC_MESSAGES/django.mo | Bin 0 -> 24445 bytes .../locale/bg_BG/LC_MESSAGES/django.po | 4857 ++++++++++++++++ .../locale/cs/LC_MESSAGES/django.mo | Bin 0 -> 1588 bytes .../locale/cs/LC_MESSAGES/django.po | 4746 ++++++++++++++++ .../locale/en/LC_MESSAGES/django.mo | Bin 0 -> 36006 bytes .../locale/en/LC_MESSAGES/django.po | 4908 ++++++++++++++++ .../locale/es/LC_MESSAGES/django.mo | Bin 0 -> 53340 bytes .../locale/es/LC_MESSAGES/django.po | 4976 ++++++++++++++++ .../locale/fr/LC_MESSAGES/django.mo | Bin 0 -> 1520 bytes .../locale/fr/LC_MESSAGES/django.po | 4719 +++++++++++++++ .../locale/it/LC_MESSAGES/django.mo | Bin 0 -> 26275 bytes .../locale/it/LC_MESSAGES/django.po | 4885 ++++++++++++++++ .../locale/ja/LC_MESSAGES/django.mo | Bin 0 -> 60197 bytes .../locale/ja/LC_MESSAGES/django.po | 4964 ++++++++++++++++ .../locale/ko_KR/LC_MESSAGES/django.mo | Bin 0 -> 2705 bytes .../locale/ko_KR/LC_MESSAGES/django.po | 4772 ++++++++++++++++ .../locale/nl_NL/LC_MESSAGES/django.mo | Bin 0 -> 9678 bytes .../locale/nl_NL/LC_MESSAGES/django.po | 4825 ++++++++++++++++ .../locale/pl/LC_MESSAGES/django.mo | Bin 0 -> 810 bytes .../locale/pl/LC_MESSAGES/django.po | 5042 +++++++++++++++++ .../locale/pt/LC_MESSAGES/django.mo | Bin 0 -> 30497 bytes .../locale/pt/LC_MESSAGES/django.po | 4903 ++++++++++++++++ .../locale/pt_BR/LC_MESSAGES/django.mo | Bin 0 -> 36757 bytes .../locale/pt_BR/LC_MESSAGES/django.po | 4918 ++++++++++++++++ .../locale/ru/LC_MESSAGES/django.mo | Bin 0 -> 494 bytes .../locale/ru/LC_MESSAGES/django.po | 4717 +++++++++++++++ .../locale/zh_CN/LC_MESSAGES/django.mo | Bin 0 -> 14112 bytes .../locale/zh_CN/LC_MESSAGES/django.po | 4817 ++++++++++++++++ .../locale/zh_TW/LC_MESSAGES/django.mo | Bin 0 -> 45327 bytes .../locale/zh_TW/LC_MESSAGES/django.po | 4893 ++++++++++++++++ .../openstack_dashboard/openstack/__init__.py | 0 .../openstack/common/__init__.py | 0 .../openstack/common/setup.py | 335 ++ .../openstack/common/version.py | 86 + dashboard/openstack_dashboard/settings.py | 215 + .../img/glyphicons-halflings-white.png | Bin 0 -> 4352 bytes .../bootstrap/img/glyphicons-halflings.png | Bin 0 -> 4352 bytes .../static/bootstrap/less/accordion.less | 28 + .../static/bootstrap/less/alerts.less | 70 + .../static/bootstrap/less/bootstrap.less | 62 + .../static/bootstrap/less/breadcrumbs.less | 22 + .../static/bootstrap/less/button-groups.less | 148 + .../static/bootstrap/less/buttons.less | 183 + .../static/bootstrap/less/carousel.less | 121 + .../static/bootstrap/less/close.less | 18 + .../static/bootstrap/less/code.less | 57 + .../bootstrap/less/component-animations.less | 18 + .../static/bootstrap/less/dropdowns.less | 130 + .../static/bootstrap/less/forms.less | 522 ++ .../static/bootstrap/less/grid.less | 8 + .../static/bootstrap/less/hero-unit.less | 20 + .../static/bootstrap/less/labels.less | 32 + .../static/bootstrap/less/layouts.less | 17 + .../static/bootstrap/less/mixins.less | 590 ++ .../static/bootstrap/less/modals.less | 83 + .../static/bootstrap/less/navbar.less | 299 + .../static/bootstrap/less/navs.less | 353 ++ .../static/bootstrap/less/pager.less | 30 + .../static/bootstrap/less/pagination.less | 55 + .../static/bootstrap/less/popovers.less | 49 + .../static/bootstrap/less/progress-bars.less | 95 + .../static/bootstrap/less/reset.less | 126 + .../static/bootstrap/less/responsive.less | 327 ++ .../static/bootstrap/less/scaffolding.less | 29 + .../static/bootstrap/less/sprites.less | 158 + .../static/bootstrap/less/tables.less | 150 + .../static/bootstrap/less/thumbnails.less | 35 + .../static/bootstrap/less/tooltip.less | 35 + .../static/bootstrap/less/type.less | 222 + .../static/bootstrap/less/utilities.less | 23 + .../static/bootstrap/less/variables.less | 107 + .../static/bootstrap/less/wells.less | 17 + .../fonts/Anivers_Regular-webfont.eot | Bin 0 -> 25687 bytes .../fonts/Anivers_Regular-webfont.svg | 244 + .../fonts/Anivers_Regular-webfont.ttf | Bin 0 -> 56996 bytes .../fonts/Anivers_Regular-webfont.woff | Bin 0 -> 29432 bytes .../static/dashboard/img/action_required.png | Bin 0 -> 1017 bytes .../static/dashboard/img/drag.png | Bin 0 -> 1023 bytes .../static/dashboard/img/drop_arrow.png | Bin 0 -> 2857 bytes .../static/dashboard/img/favicon.ico | Bin 0 -> 1150 bytes .../static/dashboard/img/loading.gif | Bin 0 -> 2947 bytes .../static/dashboard/img/logo-splash.png | Bin 0 -> 5093 bytes .../static/dashboard/img/logo.png | Bin 0 -> 5093 bytes .../static/dashboard/img/right_droparrow.png | Bin 0 -> 991 bytes .../static/dashboard/img/router.png | Bin 0 -> 2321 bytes .../static/dashboard/img/search.png | Bin 0 -> 431 bytes .../static/dashboard/img/server.png | Bin 0 -> 2050 bytes .../static/dashboard/img/up_arrow.png | Bin 0 -> 974 bytes .../static/dashboard/less/horizon.less | 2076 +++++++ .../openstack_dashboard/templates/403.html | 28 + .../openstack_dashboard/templates/404.html | 27 + .../openstack_dashboard/templates/500.html | 82 + .../templates/_header.html | 10 + .../templates/_stylesheets.html | 7 + .../openstack_dashboard/test/__init__.py | 0 .../test/api_tests/__init__.py | 0 .../test/api_tests/base_tests.py | 149 + .../test/api_tests/cinder_tests.py | 56 + .../test/api_tests/glance_tests.py | 121 + .../test/api_tests/keystone_tests.py | 98 + .../test/api_tests/lbaas_tests.py | 363 ++ .../test/api_tests/network_tests.py | 291 + .../test/api_tests/nova_tests.py | 173 + .../test/api_tests/quantum_tests.py | 272 + .../test/api_tests/swift_tests.py | 121 + .../test/error_pages_urls.py | 7 + dashboard/openstack_dashboard/test/helpers.py | 363 ++ .../openstack_dashboard/test/settings.py | 98 + .../test/templates/404.html | 1 + .../test/templates/500.html | 1 + .../test/templates/_tab.html | 1 + .../test/templates/base-sidebar.html | 0 .../test/templates/registration/login.html | 0 .../test/templates/tab_group.html | 1 + .../test/templates/workflow.html | 1 + .../test/test_data/__init__.py | 0 .../test/test_data/exceptions.py | 67 + .../test/test_data/glance_data.py | 117 + .../test/test_data/keystone_data.py | 170 + .../test/test_data/nova_data.py | 428 ++ .../test/test_data/quantum_data.py | 355 ++ .../test/test_data/swift_data.py | 39 + .../test/test_data/utils.py | 126 + .../test/tests/__init__.py | 0 .../test/tests/error_pages.py | 33 + .../openstack_dashboard/test/tests/quotas.py | 189 + .../test/tests/selenium.py | 24 + dashboard/openstack_dashboard/urls.py | 50 + .../openstack_dashboard/usage/__init__.py | 19 + dashboard/openstack_dashboard/usage/base.py | 157 + dashboard/openstack_dashboard/usage/quotas.py | 127 + dashboard/openstack_dashboard/usage/tables.py | 71 + dashboard/openstack_dashboard/usage/views.py | 54 + dashboard/openstack_dashboard/views.py | 38 + .../openstack_dashboard/wsgi/django.wsgi | 15 + dashboard/run_tests.sh | 442 ++ dashboard/setup.cfg | 9 + dashboard/setup.py | 107 + dashboard/tools/install_venv.py | 156 + dashboard/tools/pip-requires | 15 + dashboard/tools/rfc.sh | 145 + dashboard/tools/test-requires | 18 + dashboard/tools/with_venv.sh | 4 + dashboard/tox.ini | 46 + 915 files changed, 151039 insertions(+), 31 deletions(-) create mode 100644 dashboard/.gitignore create mode 100644 dashboard/.gitreview create mode 100644 dashboard/.mailmap create mode 100644 dashboard/.pep8 create mode 100644 dashboard/.pylintrc create mode 100644 dashboard/.tx/config create mode 100644 dashboard/LICENSE create mode 100644 dashboard/MANIFEST.in create mode 100644 dashboard/Makefile create mode 100644 dashboard/README.rst delete mode 100644 dashboard/ReadMe.txt create mode 100755 dashboard/bin/less/lessc create mode 100644 dashboard/bin/lib/less/browser.js create mode 100644 dashboard/bin/lib/less/colors.js create mode 100644 dashboard/bin/lib/less/cssmin.js create mode 100644 dashboard/bin/lib/less/functions.js create mode 100644 dashboard/bin/lib/less/index.js create mode 100644 dashboard/bin/lib/less/parser.js create mode 100644 dashboard/bin/lib/less/rhino.js create mode 100644 dashboard/bin/lib/less/tree.js create mode 100644 dashboard/bin/lib/less/tree/alpha.js create mode 100644 dashboard/bin/lib/less/tree/anonymous.js create mode 100644 dashboard/bin/lib/less/tree/assignment.js create mode 100644 dashboard/bin/lib/less/tree/call.js create mode 100644 dashboard/bin/lib/less/tree/color.js create mode 100644 dashboard/bin/lib/less/tree/comment.js create mode 100644 dashboard/bin/lib/less/tree/condition.js create mode 100644 dashboard/bin/lib/less/tree/dimension.js create mode 100644 dashboard/bin/lib/less/tree/directive.js create mode 100644 dashboard/bin/lib/less/tree/element.js create mode 100644 dashboard/bin/lib/less/tree/expression.js create mode 100644 dashboard/bin/lib/less/tree/import.js create mode 100644 dashboard/bin/lib/less/tree/javascript.js create mode 100644 dashboard/bin/lib/less/tree/keyword.js create mode 100644 dashboard/bin/lib/less/tree/media.js create mode 100644 dashboard/bin/lib/less/tree/mixin.js create mode 100644 dashboard/bin/lib/less/tree/operation.js create mode 100644 dashboard/bin/lib/less/tree/paren.js create mode 100644 dashboard/bin/lib/less/tree/quoted.js create mode 100644 dashboard/bin/lib/less/tree/rule.js create mode 100644 dashboard/bin/lib/less/tree/ruleset.js create mode 100644 dashboard/bin/lib/less/tree/selector.js create mode 100644 dashboard/bin/lib/less/tree/url.js create mode 100644 dashboard/bin/lib/less/tree/value.js create mode 100644 dashboard/bin/lib/less/tree/variable.js create mode 100644 dashboard/doc/Makefile rename dashboard/{windc/__init__.py => doc/source/_static/.gitignore} (100%) create mode 100644 dashboard/doc/source/_static/basic.css create mode 100644 dashboard/doc/source/_static/default.css create mode 100644 dashboard/doc/source/_static/header-line.gif create mode 100644 dashboard/doc/source/_static/header_bg.jpg create mode 100644 dashboard/doc/source/_static/jquery.tweet.js create mode 100644 dashboard/doc/source/_static/nature.css create mode 100644 dashboard/doc/source/_static/openstack_logo.png create mode 100644 dashboard/doc/source/_static/tweaks.css create mode 100644 dashboard/doc/source/_templates/.placeholder create mode 100644 dashboard/doc/source/_theme/layout.html create mode 100644 dashboard/doc/source/_theme/theme.conf create mode 100644 dashboard/doc/source/conf.py create mode 100644 dashboard/doc/source/contributing.rst create mode 100644 dashboard/doc/source/faq.rst create mode 100644 dashboard/doc/source/glossary.rst create mode 100644 dashboard/doc/source/index.rst create mode 100644 dashboard/doc/source/intro.rst create mode 100644 dashboard/doc/source/quickstart.rst create mode 100644 dashboard/doc/source/ref/context_processors.rst create mode 100644 dashboard/doc/source/ref/decorators.rst create mode 100644 dashboard/doc/source/ref/exceptions.rst create mode 100644 dashboard/doc/source/ref/forms.rst create mode 100644 dashboard/doc/source/ref/horizon.rst create mode 100644 dashboard/doc/source/ref/middleware.rst create mode 100644 dashboard/doc/source/ref/run_tests.rst create mode 100644 dashboard/doc/source/ref/tables.rst create mode 100644 dashboard/doc/source/ref/tabs.rst create mode 100644 dashboard/doc/source/ref/test.rst create mode 100644 dashboard/doc/source/ref/workflows.rst create mode 100644 dashboard/doc/source/releases/2012_1.rst create mode 100644 dashboard/doc/source/releases/2012_2.rst create mode 100644 dashboard/doc/source/testing.rst create mode 100644 dashboard/doc/source/topics/customizing.rst create mode 100644 dashboard/doc/source/topics/deployment.rst create mode 100644 dashboard/doc/source/topics/tables.rst create mode 100644 dashboard/doc/source/topics/testing.rst create mode 100644 dashboard/doc/source/topics/tutorial.rst create mode 100644 dashboard/horizon/__init__.py create mode 100644 dashboard/horizon/base.py create mode 100644 dashboard/horizon/browsers/__init__.py create mode 100644 dashboard/horizon/browsers/base.py create mode 100644 dashboard/horizon/browsers/breadcrumb.py create mode 100644 dashboard/horizon/browsers/views.py create mode 100644 dashboard/horizon/conf/__init__.py create mode 100644 dashboard/horizon/conf/dash_template/__init__.py create mode 100644 dashboard/horizon/conf/dash_template/dashboard.py.tmpl create mode 100644 dashboard/horizon/conf/dash_template/models.py create mode 100644 dashboard/horizon/conf/dash_template/static/dash_name/css/dash_name.css create mode 100644 dashboard/horizon/conf/dash_template/static/dash_name/js/dash_name.js create mode 100644 dashboard/horizon/conf/dash_template/templates/dash_name/base.html create mode 100644 dashboard/horizon/conf/default.py create mode 100644 dashboard/horizon/conf/panel_template/__init__.py create mode 100644 dashboard/horizon/conf/panel_template/models.py create mode 100644 dashboard/horizon/conf/panel_template/panel.py.tmpl create mode 100644 dashboard/horizon/conf/panel_template/templates/panel_name/index.html create mode 100644 dashboard/horizon/conf/panel_template/tests.py.tmpl create mode 100644 dashboard/horizon/conf/panel_template/urls.py create mode 100644 dashboard/horizon/conf/panel_template/views.py create mode 100644 dashboard/horizon/context_processors.py create mode 100644 dashboard/horizon/decorators.py create mode 100644 dashboard/horizon/exceptions.py create mode 100644 dashboard/horizon/forms/__init__.py create mode 100644 dashboard/horizon/forms/base.py create mode 100644 dashboard/horizon/forms/fields.py create mode 100644 dashboard/horizon/forms/views.py create mode 100644 dashboard/horizon/loaders.py create mode 100644 dashboard/horizon/locale/bg_BG/LC_MESSAGES/django.mo create mode 100644 dashboard/horizon/locale/bg_BG/LC_MESSAGES/django.po create mode 100644 dashboard/horizon/locale/bg_BG/LC_MESSAGES/djangojs.mo create mode 100644 dashboard/horizon/locale/bg_BG/LC_MESSAGES/djangojs.po create mode 100644 dashboard/horizon/locale/en/LC_MESSAGES/django.mo create mode 100644 dashboard/horizon/locale/en/LC_MESSAGES/django.po create mode 100644 dashboard/horizon/locale/en/LC_MESSAGES/djangojs.mo create mode 100644 dashboard/horizon/locale/en/LC_MESSAGES/djangojs.po create mode 100644 dashboard/horizon/locale/es/LC_MESSAGES/django.mo create mode 100644 dashboard/horizon/locale/es/LC_MESSAGES/django.po create mode 100644 dashboard/horizon/locale/es/LC_MESSAGES/djangojs.mo create mode 100644 dashboard/horizon/locale/es/LC_MESSAGES/djangojs.po create mode 100644 dashboard/horizon/locale/fr/LC_MESSAGES/django.mo create mode 100644 dashboard/horizon/locale/fr/LC_MESSAGES/django.po create mode 100644 dashboard/horizon/locale/fr/LC_MESSAGES/djangojs.mo create mode 100644 dashboard/horizon/locale/fr/LC_MESSAGES/djangojs.po create mode 100644 dashboard/horizon/locale/it/LC_MESSAGES/django.mo create mode 100644 dashboard/horizon/locale/it/LC_MESSAGES/django.po create mode 100644 dashboard/horizon/locale/it/LC_MESSAGES/djangojs.mo create mode 100644 dashboard/horizon/locale/it/LC_MESSAGES/djangojs.po create mode 100644 dashboard/horizon/locale/ja/LC_MESSAGES/django.mo create mode 100644 dashboard/horizon/locale/ja/LC_MESSAGES/django.po create mode 100644 dashboard/horizon/locale/ja/LC_MESSAGES/djangojs.mo create mode 100644 dashboard/horizon/locale/ja/LC_MESSAGES/djangojs.po create mode 100644 dashboard/horizon/locale/ko_KR/LC_MESSAGES/django.mo create mode 100644 dashboard/horizon/locale/ko_KR/LC_MESSAGES/django.po create mode 100644 dashboard/horizon/locale/ko_KR/LC_MESSAGES/djangojs.mo create mode 100644 dashboard/horizon/locale/ko_KR/LC_MESSAGES/djangojs.po create mode 100644 dashboard/horizon/locale/nl_NL/LC_MESSAGES/django.mo create mode 100644 dashboard/horizon/locale/nl_NL/LC_MESSAGES/django.po create mode 100644 dashboard/horizon/locale/nl_NL/LC_MESSAGES/djangojs.mo create mode 100644 dashboard/horizon/locale/nl_NL/LC_MESSAGES/djangojs.po create mode 100644 dashboard/horizon/locale/pl/LC_MESSAGES/django.mo create mode 100644 dashboard/horizon/locale/pl/LC_MESSAGES/django.po create mode 100644 dashboard/horizon/locale/pl/LC_MESSAGES/djangojs.mo create mode 100644 dashboard/horizon/locale/pl/LC_MESSAGES/djangojs.po create mode 100644 dashboard/horizon/locale/pt/LC_MESSAGES/django.mo create mode 100644 dashboard/horizon/locale/pt/LC_MESSAGES/django.po create mode 100644 dashboard/horizon/locale/pt/LC_MESSAGES/djangojs.mo create mode 100644 dashboard/horizon/locale/pt/LC_MESSAGES/djangojs.po create mode 100644 dashboard/horizon/locale/pt_BR/LC_MESSAGES/django.mo create mode 100644 dashboard/horizon/locale/pt_BR/LC_MESSAGES/django.po create mode 100644 dashboard/horizon/locale/pt_BR/LC_MESSAGES/djangojs.mo create mode 100644 dashboard/horizon/locale/pt_BR/LC_MESSAGES/djangojs.po create mode 100644 dashboard/horizon/locale/ru/LC_MESSAGES/django.mo create mode 100644 dashboard/horizon/locale/ru/LC_MESSAGES/django.po create mode 100644 dashboard/horizon/locale/ru/LC_MESSAGES/djangojs.mo create mode 100644 dashboard/horizon/locale/ru/LC_MESSAGES/djangojs.po create mode 100644 dashboard/horizon/locale/zh_CN/LC_MESSAGES/django.mo create mode 100644 dashboard/horizon/locale/zh_CN/LC_MESSAGES/django.po create mode 100644 dashboard/horizon/locale/zh_CN/LC_MESSAGES/djangojs.mo create mode 100644 dashboard/horizon/locale/zh_CN/LC_MESSAGES/djangojs.po create mode 100644 dashboard/horizon/locale/zh_TW/LC_MESSAGES/django.mo create mode 100644 dashboard/horizon/locale/zh_TW/LC_MESSAGES/django.po create mode 100644 dashboard/horizon/locale/zh_TW/LC_MESSAGES/djangojs.mo create mode 100644 dashboard/horizon/locale/zh_TW/LC_MESSAGES/djangojs.po create mode 100644 dashboard/horizon/management/__init__.py create mode 100644 dashboard/horizon/management/commands/__init__.py create mode 100644 dashboard/horizon/management/commands/startdash.py create mode 100644 dashboard/horizon/management/commands/startpanel.py create mode 100644 dashboard/horizon/messages.py create mode 100644 dashboard/horizon/middleware.py create mode 100644 dashboard/horizon/models.py create mode 100644 dashboard/horizon/site_urls.py create mode 100644 dashboard/horizon/static/bootstrap/js/bootstrap.js create mode 100644 dashboard/horizon/static/bootstrap/js/bootstrap.min.js create mode 100644 dashboard/horizon/static/horizon/js/horizon.communication.js create mode 100644 dashboard/horizon/static/horizon/js/horizon.conf.js create mode 100644 dashboard/horizon/static/horizon/js/horizon.cookies.js create mode 100644 dashboard/horizon/static/horizon/js/horizon.forms.js create mode 100644 dashboard/horizon/static/horizon/js/horizon.instances.js create mode 100644 dashboard/horizon/static/horizon/js/horizon.js create mode 100644 dashboard/horizon/static/horizon/js/horizon.messages.js create mode 100644 dashboard/horizon/static/horizon/js/horizon.modals.js create mode 100644 dashboard/horizon/static/horizon/js/horizon.networktopology.js create mode 100644 dashboard/horizon/static/horizon/js/horizon.projects.js create mode 100644 dashboard/horizon/static/horizon/js/horizon.quota.js create mode 100644 dashboard/horizon/static/horizon/js/horizon.tables.js create mode 100644 dashboard/horizon/static/horizon/js/horizon.tabs.js create mode 100644 dashboard/horizon/static/horizon/js/horizon.templates.js create mode 100644 dashboard/horizon/static/horizon/js/horizon.users.js create mode 100644 dashboard/horizon/static/horizon/js/horizon.utils.js create mode 100644 dashboard/horizon/static/horizon/lib/hogan-2.0.0.js create mode 100755 dashboard/horizon/static/horizon/lib/jquery/jquery-ui-1.9.2.custom.min.js create mode 100644 dashboard/horizon/static/horizon/lib/jquery/jquery.cookie.js create mode 100644 dashboard/horizon/static/horizon/lib/jquery/jquery.min.js create mode 100644 dashboard/horizon/static/horizon/lib/jquery/jquery.quicksearch.js create mode 100644 dashboard/horizon/static/horizon/lib/jquery/jquery.table-sorter.js create mode 100644 dashboard/horizon/static/horizon/lib/json2.js create mode 100644 dashboard/horizon/static/horizon/lib/qunit/qunit.css create mode 100644 dashboard/horizon/static/horizon/lib/qunit/qunit.js create mode 100644 dashboard/horizon/static/horizon/lib/spin.jquery.js create mode 100644 dashboard/horizon/static/horizon/lib/spin.js create mode 100644 dashboard/horizon/static/horizon/lib/underscore/underscore-min.js create mode 100644 dashboard/horizon/static/horizon/tests/messages.js create mode 100644 dashboard/horizon/static/horizon/tests/modals.js create mode 100644 dashboard/horizon/static/horizon/tests/tables.js create mode 100644 dashboard/horizon/static/horizon/tests/templates.js create mode 100644 dashboard/horizon/tables/__init__.py create mode 100644 dashboard/horizon/tables/actions.py create mode 100644 dashboard/horizon/tables/base.py create mode 100644 dashboard/horizon/tables/views.py create mode 100644 dashboard/horizon/tabs/__init__.py create mode 100644 dashboard/horizon/tabs/base.py create mode 100644 dashboard/horizon/tabs/views.py create mode 100644 dashboard/horizon/templates/_header.html create mode 100644 dashboard/horizon/templates/_stylesheets.html create mode 100644 dashboard/horizon/templates/auth/_login.html create mode 100644 dashboard/horizon/templates/auth/login.html create mode 100644 dashboard/horizon/templates/base.html create mode 100644 dashboard/horizon/templates/horizon/_conf.html create mode 100644 dashboard/horizon/templates/horizon/_messages.html create mode 100644 dashboard/horizon/templates/horizon/_nav_list.html create mode 100644 dashboard/horizon/templates/horizon/_scripts.html create mode 100644 dashboard/horizon/templates/horizon/_subnav_list.html create mode 100644 dashboard/horizon/templates/horizon/client_side/_alert_message.html create mode 100644 dashboard/horizon/templates/horizon/client_side/_loading.html create mode 100644 dashboard/horizon/templates/horizon/client_side/_modal.html create mode 100644 dashboard/horizon/templates/horizon/client_side/_project_user.html create mode 100644 dashboard/horizon/templates/horizon/client_side/_script_loader.html create mode 100644 dashboard/horizon/templates/horizon/client_side/_table_row.html create mode 100644 dashboard/horizon/templates/horizon/client_side/template.html create mode 100644 dashboard/horizon/templates/horizon/client_side/templates.html create mode 100644 dashboard/horizon/templates/horizon/common/_breadcrumb.html create mode 100644 dashboard/horizon/templates/horizon/common/_data_table.html create mode 100644 dashboard/horizon/templates/horizon/common/_data_table_row.html create mode 100644 dashboard/horizon/templates/horizon/common/_data_table_row_action.html create mode 100644 dashboard/horizon/templates/horizon/common/_data_table_row_actions.html create mode 100644 dashboard/horizon/templates/horizon/common/_data_table_table_actions.html create mode 100644 dashboard/horizon/templates/horizon/common/_detail_table.html create mode 100644 dashboard/horizon/templates/horizon/common/_form_fields.html create mode 100644 dashboard/horizon/templates/horizon/common/_modal.html create mode 100644 dashboard/horizon/templates/horizon/common/_modal_form.html create mode 100644 dashboard/horizon/templates/horizon/common/_page_header.html create mode 100644 dashboard/horizon/templates/horizon/common/_progress_bar.html create mode 100644 dashboard/horizon/templates/horizon/common/_quota_summary.html create mode 100644 dashboard/horizon/templates/horizon/common/_region_selector.html create mode 100644 dashboard/horizon/templates/horizon/common/_resource_browser.html create mode 100644 dashboard/horizon/templates/horizon/common/_sidebar.html create mode 100644 dashboard/horizon/templates/horizon/common/_sidebar_module.html create mode 100644 dashboard/horizon/templates/horizon/common/_tab_group.html create mode 100644 dashboard/horizon/templates/horizon/common/_usage_summary.html create mode 100644 dashboard/horizon/templates/horizon/common/_workflow.html create mode 100644 dashboard/horizon/templates/horizon/common/_workflow_step.html create mode 100644 dashboard/horizon/templates/horizon/common/_workflow_step_update_members.html create mode 100644 dashboard/horizon/templates/horizon/qunit.html create mode 100644 dashboard/horizon/templates/splash.html create mode 100644 dashboard/horizon/templatetags/__init__.py create mode 100644 dashboard/horizon/templatetags/branding.py create mode 100644 dashboard/horizon/templatetags/horizon.py create mode 100644 dashboard/horizon/templatetags/parse_date.py create mode 100644 dashboard/horizon/templatetags/sizeformat.py create mode 100644 dashboard/horizon/templatetags/truncate_filter.py create mode 100644 dashboard/horizon/test/__init__.py create mode 100644 dashboard/horizon/test/helpers.py create mode 100644 dashboard/horizon/test/settings.py create mode 100644 dashboard/horizon/test/templates/404.html create mode 100644 dashboard/horizon/test/templates/_tab.html create mode 100644 dashboard/horizon/test/templates/base-sidebar.html create mode 100644 dashboard/horizon/test/templates/registration/login.html create mode 100644 dashboard/horizon/test/templates/tab_group.html create mode 100644 dashboard/horizon/test/templates/workflow.html create mode 100644 dashboard/horizon/test/test_dashboards/__init__.py create mode 100644 dashboard/horizon/test/test_dashboards/cats/__init__.py create mode 100644 dashboard/horizon/test/test_dashboards/cats/dashboard.py create mode 100644 dashboard/horizon/test/test_dashboards/cats/kittens/__init__.py create mode 100644 dashboard/horizon/test/test_dashboards/cats/kittens/models.py create mode 100644 dashboard/horizon/test/test_dashboards/cats/kittens/panel.py create mode 100644 dashboard/horizon/test/test_dashboards/cats/kittens/templates/kittens/index.html create mode 100644 dashboard/horizon/test/test_dashboards/cats/kittens/urls.py create mode 100644 dashboard/horizon/test/test_dashboards/cats/kittens/views.py create mode 100644 dashboard/horizon/test/test_dashboards/cats/models.py create mode 100644 dashboard/horizon/test/test_dashboards/cats/static/cats/css/cats.css create mode 100644 dashboard/horizon/test/test_dashboards/cats/static/cats/js/cats.js create mode 100644 dashboard/horizon/test/test_dashboards/cats/templates/cats/base.html create mode 100644 dashboard/horizon/test/test_dashboards/cats/tigers/__init__.py create mode 100644 dashboard/horizon/test/test_dashboards/cats/tigers/models.py create mode 100644 dashboard/horizon/test/test_dashboards/cats/tigers/panel.py create mode 100644 dashboard/horizon/test/test_dashboards/cats/tigers/templates/tigers/index.html create mode 100644 dashboard/horizon/test/test_dashboards/cats/tigers/urls.py create mode 100644 dashboard/horizon/test/test_dashboards/cats/tigers/views.py create mode 100644 dashboard/horizon/test/test_dashboards/dogs/__init__.py create mode 100644 dashboard/horizon/test/test_dashboards/dogs/dashboard.py create mode 100644 dashboard/horizon/test/test_dashboards/dogs/models.py create mode 100644 dashboard/horizon/test/test_dashboards/dogs/puppies/__init__.py create mode 100644 dashboard/horizon/test/test_dashboards/dogs/puppies/models.py create mode 100644 dashboard/horizon/test/test_dashboards/dogs/puppies/panel.py create mode 100644 dashboard/horizon/test/test_dashboards/dogs/puppies/templates/puppies/index.html create mode 100644 dashboard/horizon/test/test_dashboards/dogs/puppies/urls.py create mode 100644 dashboard/horizon/test/test_dashboards/dogs/puppies/views.py create mode 100644 dashboard/horizon/test/test_dashboards/dogs/static/dogs/css/dogs.css create mode 100644 dashboard/horizon/test/test_dashboards/dogs/static/dogs/js/dogs.js create mode 100644 dashboard/horizon/test/test_dashboards/dogs/templates/dogs/base.html create mode 100644 dashboard/horizon/test/tests/__init__.py create mode 100644 dashboard/horizon/test/tests/base.py create mode 100644 dashboard/horizon/test/tests/messages.py create mode 100644 dashboard/horizon/test/tests/middleware.py create mode 100644 dashboard/horizon/test/tests/selenium.py create mode 100644 dashboard/horizon/test/tests/tables.py create mode 100644 dashboard/horizon/test/tests/tabs.py create mode 100644 dashboard/horizon/test/tests/templatetags.py create mode 100644 dashboard/horizon/test/tests/utils.py create mode 100644 dashboard/horizon/test/tests/workflows.py create mode 100644 dashboard/horizon/test/urls.py create mode 100644 dashboard/horizon/utils/__init__.py create mode 100644 dashboard/horizon/utils/fields.py create mode 100644 dashboard/horizon/utils/filters.py create mode 100644 dashboard/horizon/utils/functions.py create mode 100644 dashboard/horizon/utils/html.py create mode 100644 dashboard/horizon/utils/memoized.py create mode 100644 dashboard/horizon/utils/secret_key.py create mode 100644 dashboard/horizon/utils/validators.py create mode 100644 dashboard/horizon/version.py create mode 100644 dashboard/horizon/views.py create mode 100644 dashboard/horizon/workflows/__init__.py create mode 100644 dashboard/horizon/workflows/base.py create mode 100644 dashboard/horizon/workflows/views.py create mode 100755 dashboard/manage.py create mode 100644 dashboard/openstack-common.conf create mode 100644 dashboard/openstack_dashboard/__init__.py create mode 100644 dashboard/openstack_dashboard/api/__init__.py create mode 100644 dashboard/openstack_dashboard/api/base.py create mode 100644 dashboard/openstack_dashboard/api/cinder.py create mode 100644 dashboard/openstack_dashboard/api/glance.py create mode 100644 dashboard/openstack_dashboard/api/keystone.py create mode 100644 dashboard/openstack_dashboard/api/lbaas.py create mode 100644 dashboard/openstack_dashboard/api/network.py create mode 100644 dashboard/openstack_dashboard/api/nova.py create mode 100644 dashboard/openstack_dashboard/api/quantum.py create mode 100644 dashboard/openstack_dashboard/api/swift.py rename dashboard/{ => openstack_dashboard}/api/windc.py (100%) create mode 100644 dashboard/openstack_dashboard/context_processors.py create mode 100644 dashboard/openstack_dashboard/dashboards/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/dashboard.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/extras/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/extras/forms.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/extras/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/extras/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/extras/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/extras/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/forms.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/panel.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/templates/flavors/_create.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/templates/flavors/_edit.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/templates/flavors/create.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/templates/flavors/edit.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/_create.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/_edit.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/_index.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/create.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/edit.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/index.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/templates/flavors/index.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/flavors/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/images/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/images/forms.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/images/panel.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/images/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/images/templates/images/_create.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/images/templates/images/_update.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/images/templates/images/create.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/images/templates/images/index.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/images/templates/images/update.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/images/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/images/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/images/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/info/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/info/panel.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/info/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/info/tabs.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/info/templates/info/index.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/info/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/info/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/info/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/instances/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/instances/panel.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/instances/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/instances/templates/instances/index.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/instances/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/instances/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/instances/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/models.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/forms.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/panel.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/ports/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/ports/forms.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/ports/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/ports/tabs.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/ports/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/ports/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/subnets/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/subnets/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/subnets/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/subnets/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/subnets/workflows.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/templates/networks/_create.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/templates/networks/_update.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/templates/networks/create.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/templates/networks/index.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/templates/networks/ports/_create.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/templates/networks/ports/_update.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/templates/networks/ports/create.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/templates/networks/ports/update.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/templates/networks/subnets/create.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/templates/networks/subnets/index.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/templates/networks/subnets/update.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/templates/networks/update.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/networks/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/overview/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/overview/panel.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/overview/templates/overview/usage.csv create mode 100644 dashboard/openstack_dashboard/dashboards/admin/overview/templates/overview/usage.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/overview/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/overview/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/overview/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/forms.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/panel.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/templates/projects/_add_user.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/templates/projects/_create.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/templates/projects/_create_user.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/templates/projects/_quotas.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/templates/projects/_update.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/templates/projects/_update_members.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/templates/projects/add_user.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/templates/projects/create.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/templates/projects/create_user.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/templates/projects/index.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/templates/projects/quotas.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/templates/projects/update.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/templates/projects/usage.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/templates/projects/users.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/projects/workflows.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/routers/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/routers/panel.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/routers/ports/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/routers/ports/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/routers/ports/tabs.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/routers/ports/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/routers/ports/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/routers/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/routers/tabs.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/routers/templates/routers/_detail_overview.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/routers/templates/routers/detail.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/routers/templates/routers/index.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/routers/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/routers/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/routers/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/users/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/users/forms.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/users/panel.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/users/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/users/templates/users/_create.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/users/templates/users/_update.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/users/templates/users/create.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/users/templates/users/index.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/users/templates/users/update.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/users/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/users/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/users/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/volumes/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/volumes/forms.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/volumes/panel.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/volumes/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/volumes/templates/volumes/_create_volume_type.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/volumes/templates/volumes/create_volume_type.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/volumes/templates/volumes/detail.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/volumes/templates/volumes/index.html create mode 100644 dashboard/openstack_dashboard/dashboards/admin/volumes/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/volumes/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/admin/volumes/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/api_access/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/api_access/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/api_access/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/api_access/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/api_access/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/floating_ips/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/floating_ips/forms.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/floating_ips/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/floating_ips/utils.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/floating_ips/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/floating_ips/workflows.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/keypairs/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/keypairs/forms.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/keypairs/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/keypairs/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/keypairs/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/keypairs/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/panel.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/security_groups/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/security_groups/forms.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/security_groups/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/security_groups/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/security_groups/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/security_groups/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/tabs.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/api_access/ec2rc.sh.template create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/api_access/openrc.sh.template create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/floating_ips/_allocate.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/floating_ips/allocate.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/floating_ips/associate.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/index.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/_create.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/_import.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/create.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/download.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/import.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/security_groups/_add_rule.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/security_groups/_create.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/security_groups/add_rule.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/security_groups/create.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/security_groups/detail.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/access_and_security/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/containers/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/containers/browsers.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/containers/forms.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/containers/panel.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/containers/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/containers/templates/containers/_copy.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/containers/templates/containers/_create.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/containers/templates/containers/_upload.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/containers/templates/containers/copy.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/containers/templates/containers/create.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/containers/templates/containers/index.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/containers/templates/containers/upload.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/containers/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/containers/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/containers/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/dashboard.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/images/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/images/forms.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/images/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/images/tabs.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/images/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/images/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/images/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/panel.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/snapshots/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/snapshots/forms.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/snapshots/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/snapshots/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/snapshots/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/snapshots/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/templates/images_and_snapshots/images/_create.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/templates/images_and_snapshots/images/_detail_overview.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/templates/images_and_snapshots/images/_update.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/templates/images_and_snapshots/images/create.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/templates/images_and_snapshots/images/detail.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/templates/images_and_snapshots/images/update.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/templates/images_and_snapshots/index.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/templates/images_and_snapshots/snapshots/_create.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/templates/images_and_snapshots/snapshots/_detail_overview.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/templates/images_and_snapshots/snapshots/create.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/templates/images_and_snapshots/snapshots/detail.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/volume_snapshots/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/volume_snapshots/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/volume_snapshots/tabs.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/images_and_snapshots/volume_snapshots/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/panel.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/tabs.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/templates/instances/_detail_console.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/templates/instances/_detail_log.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/templates/instances/_detail_overview.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/templates/instances/_instance_ips.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/templates/instances/_launch_customize_help.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/templates/instances/_launch_details_help.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/templates/instances/_launch_network_help.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/templates/instances/_launch_volumes_help.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/templates/instances/_update_networks.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/templates/instances/detail.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/templates/instances/index.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/templates/instances/launch.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/templates/instances/update.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/workflows/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/instances/workflows/update_instance.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/loadbalancers/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/loadbalancers/models.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/loadbalancers/panel.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/loadbalancers/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/loadbalancers/tabs.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_member_details.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_members_tab.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_monitor_details.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_monitors_tab.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_pool_details.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_pools_tab.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/_vip_details.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/addmember.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/addmonitor.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/addpool.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/addvip.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/loadbalancers/templates/loadbalancers/details_tabs.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/loadbalancers/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/loadbalancers/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/loadbalancers/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/loadbalancers/workflows.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/models.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/network_topology/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/network_topology/panel.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/network_topology/templates/network_topology/index.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/network_topology/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/network_topology/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/forms.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/panel.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/ports/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/ports/forms.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/ports/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/ports/tabs.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/ports/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/ports/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/subnets/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/subnets/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/subnets/tabs.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/subnets/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/subnets/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/subnets/workflows.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/templates/networks/_create.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/templates/networks/_detail_overview.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/templates/networks/_network_ips.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/templates/networks/_update.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/templates/networks/create.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/templates/networks/detail.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/templates/networks/index.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_detail_overview.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_port_ips.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/templates/networks/ports/_update.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/templates/networks/ports/detail.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/templates/networks/ports/update.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/_detail_overview.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/create.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/detail.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/index.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/templates/networks/subnets/update.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/templates/networks/update.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/networks/workflows.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/overview/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/overview/panel.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/overview/templates/overview/usage.csv create mode 100644 dashboard/openstack_dashboard/dashboards/project/overview/templates/overview/usage.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/overview/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/overview/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/overview/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/forms.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/panel.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/ports/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/ports/forms.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/ports/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/ports/tabs.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/ports/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/ports/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/tabs.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/templates/routers/_create.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/templates/routers/_detail_overview.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/templates/routers/create.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/templates/routers/detail.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/templates/routers/index.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/templates/routers/ports/_create.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/templates/routers/ports/_setgateway.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/templates/routers/ports/create.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/templates/routers/ports/setgateway.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/routers/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/volumes/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/volumes/forms.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/volumes/panel.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/volumes/tables.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/volumes/tabs.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/volumes/templates/volumes/_attach.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/volumes/templates/volumes/_create.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/volumes/templates/volumes/_create_snapshot.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/volumes/templates/volumes/_detail_overview.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/volumes/templates/volumes/attach.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/volumes/templates/volumes/create.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/volumes/templates/volumes/create_snapshot.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/volumes/templates/volumes/detail.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/volumes/templates/volumes/index.html create mode 100644 dashboard/openstack_dashboard/dashboards/project/volumes/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/volumes/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/volumes/views.py create mode 100644 dashboard/openstack_dashboard/dashboards/project/windc/.idea/.name create mode 100644 dashboard/openstack_dashboard/dashboards/project/windc/.idea/encodings.xml create mode 100644 dashboard/openstack_dashboard/dashboards/project/windc/.idea/misc.xml create mode 100644 dashboard/openstack_dashboard/dashboards/project/windc/.idea/modules.xml create mode 100644 dashboard/openstack_dashboard/dashboards/project/windc/.idea/other.xml create mode 100644 dashboard/openstack_dashboard/dashboards/project/windc/.idea/scopes/scope_settings.xml create mode 100644 dashboard/openstack_dashboard/dashboards/project/windc/.idea/testrunner.xml create mode 100644 dashboard/openstack_dashboard/dashboards/project/windc/.idea/vcs.xml create mode 100644 dashboard/openstack_dashboard/dashboards/project/windc/.idea/windc.iml create mode 100644 dashboard/openstack_dashboard/dashboards/project/windc/.idea/workspace.xml create mode 100644 dashboard/openstack_dashboard/dashboards/project/windc/__init__.py rename dashboard/{ => openstack_dashboard/dashboards/project}/windc/forms.py (100%) rename dashboard/{ => openstack_dashboard/dashboards/project}/windc/panel.py (100%) rename dashboard/{ => openstack_dashboard/dashboards/project}/windc/tables.py (100%) rename dashboard/{ => openstack_dashboard/dashboards/project}/windc/tabs.py (100%) rename dashboard/{ => openstack_dashboard/dashboards/project}/windc/templates/windc/_data_center_help.html (100%) rename dashboard/{ => openstack_dashboard/dashboards/project}/windc/templates/windc/_dc_help.html (100%) rename dashboard/{ => openstack_dashboard/dashboards/project}/windc/templates/windc/_iis_help.html (100%) rename dashboard/{ => openstack_dashboard/dashboards/project}/windc/templates/windc/_services.html (100%) rename dashboard/{ => openstack_dashboard/dashboards/project}/windc/templates/windc/_services_tabs.html (100%) rename dashboard/{ => openstack_dashboard/dashboards/project}/windc/templates/windc/create.html (100%) rename dashboard/{ => openstack_dashboard/dashboards/project}/windc/templates/windc/create_dc.html (100%) rename dashboard/{ => openstack_dashboard/dashboards/project}/windc/templates/windc/index.html (100%) rename dashboard/{ => openstack_dashboard/dashboards/project}/windc/templates/windc/services.html (100%) rename dashboard/{ => openstack_dashboard/dashboards/project}/windc/templates/windc/update.html (100%) rename dashboard/{ => openstack_dashboard/dashboards/project}/windc/urls.py (100%) rename dashboard/{ => openstack_dashboard/dashboards/project}/windc/views.py (100%) rename dashboard/{ => openstack_dashboard/dashboards/project}/windc/workflows.py (100%) create mode 100644 dashboard/openstack_dashboard/dashboards/settings/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/settings/dashboard.py create mode 100644 dashboard/openstack_dashboard/dashboards/settings/models.py create mode 100644 dashboard/openstack_dashboard/dashboards/settings/user/__init__.py create mode 100644 dashboard/openstack_dashboard/dashboards/settings/user/forms.py create mode 100644 dashboard/openstack_dashboard/dashboards/settings/user/panel.py create mode 100644 dashboard/openstack_dashboard/dashboards/settings/user/templates/user/_settings.html create mode 100644 dashboard/openstack_dashboard/dashboards/settings/user/templates/user/settings.html create mode 100644 dashboard/openstack_dashboard/dashboards/settings/user/tests.py create mode 100644 dashboard/openstack_dashboard/dashboards/settings/user/urls.py create mode 100644 dashboard/openstack_dashboard/dashboards/settings/user/views.py create mode 100644 dashboard/openstack_dashboard/exceptions.py create mode 100644 dashboard/openstack_dashboard/local/__init__.py create mode 100644 dashboard/openstack_dashboard/local/local_settings.py.example create mode 100644 dashboard/openstack_dashboard/locale/bg_BG/LC_MESSAGES/django.mo create mode 100644 dashboard/openstack_dashboard/locale/bg_BG/LC_MESSAGES/django.po create mode 100644 dashboard/openstack_dashboard/locale/cs/LC_MESSAGES/django.mo create mode 100644 dashboard/openstack_dashboard/locale/cs/LC_MESSAGES/django.po create mode 100644 dashboard/openstack_dashboard/locale/en/LC_MESSAGES/django.mo create mode 100644 dashboard/openstack_dashboard/locale/en/LC_MESSAGES/django.po create mode 100644 dashboard/openstack_dashboard/locale/es/LC_MESSAGES/django.mo create mode 100644 dashboard/openstack_dashboard/locale/es/LC_MESSAGES/django.po create mode 100644 dashboard/openstack_dashboard/locale/fr/LC_MESSAGES/django.mo create mode 100644 dashboard/openstack_dashboard/locale/fr/LC_MESSAGES/django.po create mode 100644 dashboard/openstack_dashboard/locale/it/LC_MESSAGES/django.mo create mode 100644 dashboard/openstack_dashboard/locale/it/LC_MESSAGES/django.po create mode 100644 dashboard/openstack_dashboard/locale/ja/LC_MESSAGES/django.mo create mode 100644 dashboard/openstack_dashboard/locale/ja/LC_MESSAGES/django.po create mode 100644 dashboard/openstack_dashboard/locale/ko_KR/LC_MESSAGES/django.mo create mode 100644 dashboard/openstack_dashboard/locale/ko_KR/LC_MESSAGES/django.po create mode 100644 dashboard/openstack_dashboard/locale/nl_NL/LC_MESSAGES/django.mo create mode 100644 dashboard/openstack_dashboard/locale/nl_NL/LC_MESSAGES/django.po create mode 100644 dashboard/openstack_dashboard/locale/pl/LC_MESSAGES/django.mo create mode 100644 dashboard/openstack_dashboard/locale/pl/LC_MESSAGES/django.po create mode 100644 dashboard/openstack_dashboard/locale/pt/LC_MESSAGES/django.mo create mode 100644 dashboard/openstack_dashboard/locale/pt/LC_MESSAGES/django.po create mode 100644 dashboard/openstack_dashboard/locale/pt_BR/LC_MESSAGES/django.mo create mode 100644 dashboard/openstack_dashboard/locale/pt_BR/LC_MESSAGES/django.po create mode 100644 dashboard/openstack_dashboard/locale/ru/LC_MESSAGES/django.mo create mode 100644 dashboard/openstack_dashboard/locale/ru/LC_MESSAGES/django.po create mode 100644 dashboard/openstack_dashboard/locale/zh_CN/LC_MESSAGES/django.mo create mode 100644 dashboard/openstack_dashboard/locale/zh_CN/LC_MESSAGES/django.po create mode 100644 dashboard/openstack_dashboard/locale/zh_TW/LC_MESSAGES/django.mo create mode 100644 dashboard/openstack_dashboard/locale/zh_TW/LC_MESSAGES/django.po create mode 100644 dashboard/openstack_dashboard/openstack/__init__.py create mode 100644 dashboard/openstack_dashboard/openstack/common/__init__.py create mode 100644 dashboard/openstack_dashboard/openstack/common/setup.py create mode 100644 dashboard/openstack_dashboard/openstack/common/version.py create mode 100644 dashboard/openstack_dashboard/settings.py create mode 100644 dashboard/openstack_dashboard/static/bootstrap/img/glyphicons-halflings-white.png create mode 100644 dashboard/openstack_dashboard/static/bootstrap/img/glyphicons-halflings.png create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/accordion.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/alerts.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/bootstrap.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/breadcrumbs.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/button-groups.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/buttons.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/carousel.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/close.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/code.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/component-animations.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/dropdowns.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/forms.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/grid.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/hero-unit.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/labels.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/layouts.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/mixins.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/modals.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/navbar.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/navs.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/pager.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/pagination.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/popovers.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/progress-bars.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/reset.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/responsive.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/scaffolding.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/sprites.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/tables.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/thumbnails.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/tooltip.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/type.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/utilities.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/variables.less create mode 100644 dashboard/openstack_dashboard/static/bootstrap/less/wells.less create mode 100644 dashboard/openstack_dashboard/static/dashboard/fonts/Anivers_Regular-webfont.eot create mode 100644 dashboard/openstack_dashboard/static/dashboard/fonts/Anivers_Regular-webfont.svg create mode 100644 dashboard/openstack_dashboard/static/dashboard/fonts/Anivers_Regular-webfont.ttf create mode 100644 dashboard/openstack_dashboard/static/dashboard/fonts/Anivers_Regular-webfont.woff create mode 100644 dashboard/openstack_dashboard/static/dashboard/img/action_required.png create mode 100644 dashboard/openstack_dashboard/static/dashboard/img/drag.png create mode 100644 dashboard/openstack_dashboard/static/dashboard/img/drop_arrow.png create mode 100644 dashboard/openstack_dashboard/static/dashboard/img/favicon.ico create mode 100644 dashboard/openstack_dashboard/static/dashboard/img/loading.gif create mode 100644 dashboard/openstack_dashboard/static/dashboard/img/logo-splash.png create mode 100644 dashboard/openstack_dashboard/static/dashboard/img/logo.png create mode 100644 dashboard/openstack_dashboard/static/dashboard/img/right_droparrow.png create mode 100644 dashboard/openstack_dashboard/static/dashboard/img/router.png create mode 100644 dashboard/openstack_dashboard/static/dashboard/img/search.png create mode 100644 dashboard/openstack_dashboard/static/dashboard/img/server.png create mode 100644 dashboard/openstack_dashboard/static/dashboard/img/up_arrow.png create mode 100644 dashboard/openstack_dashboard/static/dashboard/less/horizon.less create mode 100644 dashboard/openstack_dashboard/templates/403.html create mode 100644 dashboard/openstack_dashboard/templates/404.html create mode 100644 dashboard/openstack_dashboard/templates/500.html create mode 100644 dashboard/openstack_dashboard/templates/_header.html create mode 100644 dashboard/openstack_dashboard/templates/_stylesheets.html create mode 100644 dashboard/openstack_dashboard/test/__init__.py create mode 100644 dashboard/openstack_dashboard/test/api_tests/__init__.py create mode 100644 dashboard/openstack_dashboard/test/api_tests/base_tests.py create mode 100644 dashboard/openstack_dashboard/test/api_tests/cinder_tests.py create mode 100644 dashboard/openstack_dashboard/test/api_tests/glance_tests.py create mode 100644 dashboard/openstack_dashboard/test/api_tests/keystone_tests.py create mode 100644 dashboard/openstack_dashboard/test/api_tests/lbaas_tests.py create mode 100644 dashboard/openstack_dashboard/test/api_tests/network_tests.py create mode 100644 dashboard/openstack_dashboard/test/api_tests/nova_tests.py create mode 100644 dashboard/openstack_dashboard/test/api_tests/quantum_tests.py create mode 100644 dashboard/openstack_dashboard/test/api_tests/swift_tests.py create mode 100644 dashboard/openstack_dashboard/test/error_pages_urls.py create mode 100644 dashboard/openstack_dashboard/test/helpers.py create mode 100644 dashboard/openstack_dashboard/test/settings.py create mode 100644 dashboard/openstack_dashboard/test/templates/404.html create mode 100644 dashboard/openstack_dashboard/test/templates/500.html create mode 100644 dashboard/openstack_dashboard/test/templates/_tab.html create mode 100644 dashboard/openstack_dashboard/test/templates/base-sidebar.html create mode 100644 dashboard/openstack_dashboard/test/templates/registration/login.html create mode 100644 dashboard/openstack_dashboard/test/templates/tab_group.html create mode 100644 dashboard/openstack_dashboard/test/templates/workflow.html create mode 100644 dashboard/openstack_dashboard/test/test_data/__init__.py create mode 100644 dashboard/openstack_dashboard/test/test_data/exceptions.py create mode 100644 dashboard/openstack_dashboard/test/test_data/glance_data.py create mode 100644 dashboard/openstack_dashboard/test/test_data/keystone_data.py create mode 100644 dashboard/openstack_dashboard/test/test_data/nova_data.py create mode 100644 dashboard/openstack_dashboard/test/test_data/quantum_data.py create mode 100644 dashboard/openstack_dashboard/test/test_data/swift_data.py create mode 100644 dashboard/openstack_dashboard/test/test_data/utils.py create mode 100644 dashboard/openstack_dashboard/test/tests/__init__.py create mode 100644 dashboard/openstack_dashboard/test/tests/error_pages.py create mode 100644 dashboard/openstack_dashboard/test/tests/quotas.py create mode 100644 dashboard/openstack_dashboard/test/tests/selenium.py create mode 100644 dashboard/openstack_dashboard/urls.py create mode 100644 dashboard/openstack_dashboard/usage/__init__.py create mode 100644 dashboard/openstack_dashboard/usage/base.py create mode 100644 dashboard/openstack_dashboard/usage/quotas.py create mode 100644 dashboard/openstack_dashboard/usage/tables.py create mode 100644 dashboard/openstack_dashboard/usage/views.py create mode 100644 dashboard/openstack_dashboard/views.py create mode 100644 dashboard/openstack_dashboard/wsgi/django.wsgi create mode 100755 dashboard/run_tests.sh create mode 100644 dashboard/setup.cfg create mode 100755 dashboard/setup.py create mode 100644 dashboard/tools/install_venv.py create mode 100644 dashboard/tools/pip-requires create mode 100755 dashboard/tools/rfc.sh create mode 100644 dashboard/tools/test-requires create mode 100755 dashboard/tools/with_venv.sh create mode 100644 dashboard/tox.ini diff --git a/dashboard/.gitignore b/dashboard/.gitignore new file mode 100644 index 00000000..463b7db5 --- /dev/null +++ b/dashboard/.gitignore @@ -0,0 +1,28 @@ +*.pyc +*.swp +*.sqlite3 +.environment_version +.selenium_log +.coverage* +.noseids +.DS_STORE +coverage.xml +nosetests.xml +pep8.txt +pylint.txt +reports +horizon.egg-info +openstack_dashboard/local/local_settings.py +openstack_dashboard/local/.secret_key_store +openstack_dashboard/test/.secret_key_store +doc/build/ +doc/source/sourcecode +/static/ +.venv +.tox +build +dist +AUTHORS +ChangeLog +tags +.idea/ \ No newline at end of file diff --git a/dashboard/.gitreview b/dashboard/.gitreview new file mode 100644 index 00000000..888950f4 --- /dev/null +++ b/dashboard/.gitreview @@ -0,0 +1,4 @@ +[gerrit] +host=review.openstack.org +port=29418 +project=openstack/horizon.git diff --git a/dashboard/.mailmap b/dashboard/.mailmap new file mode 100644 index 00000000..e42fc43f --- /dev/null +++ b/dashboard/.mailmap @@ -0,0 +1,10 @@ +# Format is: +# +# + + + + + + +Zhongyue Luo diff --git a/dashboard/.pep8 b/dashboard/.pep8 new file mode 100644 index 00000000..9e4ea374 --- /dev/null +++ b/dashboard/.pep8 @@ -0,0 +1,4 @@ + +[pep8] +ignore = E121,E126,E127,E128,W602 +exclude = vcsversion.py,panel_template,dash_template,local_settings.py diff --git a/dashboard/.pylintrc b/dashboard/.pylintrc new file mode 100644 index 00000000..64f5cafe --- /dev/null +++ b/dashboard/.pylintrc @@ -0,0 +1,42 @@ +# The format of this file isn't really documented; just use --generate-rcfile +[MASTER] +# Add to the black list. It should be a base name, not a +# path. You may set this option multiple times. +ignore=test + +[Messages Control] +# NOTE(justinsb): We might want to have a 2nd strict pylintrc in future +# C0111: Don't require docstrings on every method +# W0511: TODOs in code comments are fine. +# W0142: *args and **kwargs are fine. +# W0622: Redefining id is fine. +disable=C0111,W0511,W0142,W0622 + +[Basic] +# Variable names can be 1 to 31 characters long, with lowercase and underscores +variable-rgx=[a-z_][a-z0-9_]{0,30}$ + +# Argument names can be 2 to 31 characters long, with lowercase and underscores +argument-rgx=[a-z_][a-z0-9_]{1,30}$ + +# Method names should be at least 3 characters long +# and be lowecased with underscores +method-rgx=([a-z_][a-z0-9_]{2,50}|setUp|tearDown)$ + +# Module names matching keystone-* are ok (files in bin/) +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+)|(keystone-[a-z0-9_-]+))$ + +# Don't require docstrings on tests. +no-docstring-rgx=((__.*__)|([tT]est.*)|setUp|tearDown)$ + +[Design] +max-public-methods=100 +min-public-methods=0 +max-args=6 + +[Variables] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +# _ is used by our localization +additional-builtins=_ diff --git a/dashboard/.tx/config b/dashboard/.tx/config new file mode 100644 index 00000000..fd48163a --- /dev/null +++ b/dashboard/.tx/config @@ -0,0 +1,11 @@ +[main] +host = https://www.transifex.com + +[horizon.horizon-translations] +file_filter = horizon/locale//LC_MESSAGES/django.po +source_lang = en_US + +[horizon.openstack-dashboard-translations] +file_filter = openstack_dashboard/locale//LC_MESSAGES/django.po +source_lang = en_US + diff --git a/dashboard/LICENSE b/dashboard/LICENSE new file mode 100644 index 00000000..68c771a0 --- /dev/null +++ b/dashboard/LICENSE @@ -0,0 +1,176 @@ + + 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/dashboard/MANIFEST.in b/dashboard/MANIFEST.in new file mode 100644 index 00000000..ca016fb4 --- /dev/null +++ b/dashboard/MANIFEST.in @@ -0,0 +1,22 @@ +recursive-include bin *.js +recursive-include doc *.py *.rst *.css *.js *.html *.conf *.jpg *.gif *.png *.css_t +recursive-include horizon *.html *.css *.js *.csv *.template *.tmpl *.mo *.po +recursive-include openstack_dashboard *.html *.js *.less *.mo *.po *.example *.eot *.svg *.ttf *.woff *.png *.ico *.wsgi *.gif *.csv *.template +recursive-include tools *.py *.sh + +include AUTHORS +include ChangeLog +include LICENSE +include Makefile +include manage.py +include openstack-common.conf +include README.rst +include run_tests.sh +include tox.ini +include bin/less/lessc +include doc/Makefile +include doc/source/_templates/.placeholder +include tools/pip-requires +include tools/test-requires + +exclude openstack_dashboard/local/local_settings.py diff --git a/dashboard/Makefile b/dashboard/Makefile new file mode 100644 index 00000000..49e4f728 --- /dev/null +++ b/dashboard/Makefile @@ -0,0 +1,24 @@ +PYTHON=`which python` +DESTDIR=/ +PROJECT=horizon + +all: + @echo "make test - Run tests" + @echo "make source - Create source package" + @echo "make install - Install on local system" + @echo "make buildrpm - Generate a rpm package" + @echo "make clean - Get rid of scratch and byte files" + +source: + $(PYTHON) setup.py sdist $(COMPILE) + +install: + $(PYTHON) setup.py install --root $(DESTDIR) $(COMPILE) + +buildrpm: + $(PYTHON) setup.py bdist_rpm --post-install=rpm/postinstall --pre-uninstall=rpm/preuninstall + +clean: + $(PYTHON) setup.py clean + rm -rf build/ MANIFEST + find . -name '*.pyc' -delete diff --git a/dashboard/README.rst b/dashboard/README.rst new file mode 100644 index 00000000..edda2b77 --- /dev/null +++ b/dashboard/README.rst @@ -0,0 +1,125 @@ +============================= +Horizon (OpenStack Dashboard) +============================= + +Horizon is a Django-based project aimed at providing a complete OpenStack +Dashboard along with an extensible framework for building new dashboards +from reusable components. The ``openstack_dashboard`` module is a reference +implementation of a Django site that uses the ``horizon`` app to provide +web-based interactions with the various OpenStack projects. + +For release management: + + * https://launchpad.net/horizon + +For blueprints and feature specifications: + + * https://blueprints.launchpad.net/horizon + +For issue tracking: + + * https://bugs.launchpad.net/horizon + +Dependencies +============ + +To get started you will need to install Node.js (http://nodejs.org/) on your +machine. Node.js is used with Horizon in order to use LESS +(http://lesscss.org/) for our CSS needs. Horizon is currently using Node.js +v0.6.12. + +For Ubuntu use apt to install Node.js:: + + $ sudo apt-get install nodejs + +For other versions of Linux, please see here:: http://nodejs.org/#download for +how to install Node.js on your system. + + +Getting Started +=============== + +For local development, first create a virtualenv for the project. +In the ``tools`` directory there is a script to create one for you: + + $ python tools/install_venv.py + +Alternatively, the ``run_tests.sh`` script will also install the environment +for you and then run the full test suite to verify everything is installed +and functioning correctly. + +Now that the virtualenv is created, you need to configure your local +environment. To do this, create a ``local_settings.py`` file in the +``openstack_dashboard/local/`` directory. There is a +``local_settings.py.example`` file there that may be used as a template. + +If all is well you should able to run the development server locally: + + $ tools/with_venv.sh manage.py runserver + +or, as a shortcut:: + + $ ./run_tests.sh --runserver + + +Settings Up OpenStack +===================== + +The recommended tool for installing and configuring the core OpenStack +components is `Devstack`_. Refer to their documentation for getting +Nova, Keystone, Glance, etc. up and running. + +.. _Devstack: http://devstack.org/ + +.. note:: + + The minimum required set of OpenStack services running includes the + following: + + * Nova (compute, api, scheduler, network, *and* volume services) + * Glance + * Keystone + + Optional support is provided for Swift. + + +Development +=========== + +For development, start with the getting started instructions above. +Once you have a working virtualenv and all the necessary packages, read on. + +If dependencies are added to either ``horizon`` or ``openstack-dashboard``, +they should be added to ``tools/pip-requires``. + +The ``run_tests.sh`` script invokes tests and analyses on both of these +components in its process, and it is what Jenkins uses to verify the +stability of the project. If run before an environment is set up, it will +ask if you wish to install one. + +To run the unit tests:: + + $ ./run_tests.sh + +Building Contributor Documentation +================================== + +This documentation is written by contributors, for contributors. + +The source is maintained in the ``doc/source`` folder using +`reStructuredText`_ and built by `Sphinx`_ + +.. _reStructuredText: http://docutils.sourceforge.net/rst.html +.. _Sphinx: http://sphinx.pocoo.org/ + +* Building Automatically:: + + $ ./run_tests.sh --docs + +* Building Manually:: + + $ export DJANGO_SETTINGS_MODULE=local.local_settings + $ python doc/generate_autodoc_index.py + $ sphinx-build -b html doc/source build/sphinx/html + +Results are in the `build/sphinx/html` directory diff --git a/dashboard/ReadMe.txt b/dashboard/ReadMe.txt deleted file mode 100644 index 45d191b7..00000000 --- a/dashboard/ReadMe.txt +++ /dev/null @@ -1,31 +0,0 @@ -# TO DO: -# 1. Add new functional for services and data centers -# 2. Fix issue with list of services: services table shoudl show services for -# specific data center - -This file is described how to install new tab on horizon dashboard. -We should do the following: - 1. Copy directory 'windc' to directory '/opt/stack/horizon/openstack_dashboard/dashboards/project' - 2. Copy api/windc.py to directory '/opt/stack/horizon/openstack_dashboard/api' - 3. Copy directory 'windcclient' to directory '/opt/stack/horizon/' - 4. Edit file '/opt/stack/horizon/openstack_dashboard/dashboards/project/dashboard.py' - Add line with windc project: - - ... -class BasePanels(horizon.PanelGroup): - slug = "compute" - name = _("Manage Compute") - panels = ('overview', - 'instances', - 'volumes', - 'images_and_snapshots', - 'access_and_security', - 'networks', - 'routers', - 'windc') - - ... - - 5. Run the test Django server: - cd /opt/stack/horizon - python manage.py runserver 67.207.197.36:8080 \ No newline at end of file diff --git a/dashboard/bin/less/lessc b/dashboard/bin/less/lessc new file mode 100755 index 00000000..30ae3520 --- /dev/null +++ b/dashboard/bin/less/lessc @@ -0,0 +1,139 @@ +#!/usr/bin/env node + +var path = require('path'), + fs = require('fs'), + sys = require('util'), + os = require('os'); + +var less = require('../lib/less'); +var args = process.argv.slice(1); +var options = { + compress: false, + yuicompress: false, + optimization: 1, + silent: false, + paths: [], + color: true, + strictImports: false +}; + +args = args.filter(function (arg) { + var match; + + if (match = arg.match(/^-I(.+)$/)) { + options.paths.push(match[1]); + return false; + } + + if (match = arg.match(/^--?([a-z][0-9a-z-]*)(?:=([^\s]+))?$/i)) { arg = match[1] } + else { return arg } + + switch (arg) { + case 'v': + case 'version': + sys.puts("lessc " + less.version.join('.') + " (LESS Compiler) [JavaScript]"); + process.exit(0); + case 'verbose': + options.verbose = true; + break; + case 's': + case 'silent': + options.silent = true; + break; + case 'strict-imports': + options.strictImports = true; + break; + case 'h': + case 'help': + sys.puts("usage: lessc source [destination]"); + process.exit(0); + case 'x': + case 'compress': + options.compress = true; + break; + case 'yui-compress': + options.yuicompress = true; + break; + case 'no-color': + options.color = false; + break; + case 'include-path': + options.paths = match[2].split(os.type().match(/Windows/) ? ';' : ':') + .map(function(p) { + if (p) { + return path.resolve(process.cwd(), p); + } + }); + break; + case 'O0': options.optimization = 0; break; + case 'O1': options.optimization = 1; break; + case 'O2': options.optimization = 2; break; + } +}); + +var input = args[1]; +if (input && input != '-') { + input = path.resolve(process.cwd(), input); +} +var output = args[2]; +if (output) { + output = path.resolve(process.cwd(), output); +} + +var css, fd, tree; + +if (! input) { + sys.puts("lessc: no input files"); + process.exit(1); +} + +var parseLessFile = function (e, data) { + if (e) { + sys.puts("lessc: " + e.message); + process.exit(1); + } + + new(less.Parser)({ + paths: [path.dirname(input)].concat(options.paths), + optimization: options.optimization, + filename: input, + strictImports: options.strictImports + }).parse(data, function (err, tree) { + if (err) { + less.writeError(err, options); + process.exit(1); + } else { + try { + css = tree.toCSS({ + compress: options.compress, + yuicompress: options.yuicompress + }); + if (output) { + fd = fs.openSync(output, "w"); + fs.writeSync(fd, css, 0, "utf8"); + } else { + sys.print(css); + } + } catch (e) { + less.writeError(e, options); + process.exit(2); + } + } + }); +}; + +if (input != '-') { + fs.readFile(input, 'utf-8', parseLessFile); +} else { + process.stdin.resume(); + process.stdin.setEncoding('utf8'); + + var buffer = ''; + process.stdin.on('data', function(data) { + buffer += data; + }); + + process.stdin.on('end', function() { + parseLessFile(false, buffer); + }); +} diff --git a/dashboard/bin/lib/less/browser.js b/dashboard/bin/lib/less/browser.js new file mode 100644 index 00000000..cab913be --- /dev/null +++ b/dashboard/bin/lib/less/browser.js @@ -0,0 +1,380 @@ +// +// browser.js - client-side engine +// + +var isFileProtocol = (location.protocol === 'file:' || + location.protocol === 'chrome:' || + location.protocol === 'chrome-extension:' || + location.protocol === 'resource:'); + +less.env = less.env || (location.hostname == '127.0.0.1' || + location.hostname == '0.0.0.0' || + location.hostname == 'localhost' || + location.port.length > 0 || + isFileProtocol ? 'development' + : 'production'); + +// Load styles asynchronously (default: false) +// +// This is set to `false` by default, so that the body +// doesn't start loading before the stylesheets are parsed. +// Setting this to `true` can result in flickering. +// +less.async = false; + +// Interval between watch polls +less.poll = less.poll || (isFileProtocol ? 1000 : 1500); + +// +// Watch mode +// +less.watch = function () { return this.watchMode = true }; +less.unwatch = function () { return this.watchMode = false }; + +if (less.env === 'development') { + less.optimization = 0; + + if (/!watch/.test(location.hash)) { + less.watch(); + } + less.watchTimer = setInterval(function () { + if (less.watchMode) { + loadStyleSheets(function (e, root, _, sheet, env) { + if (root) { + createCSS(root.toCSS(), sheet, env.lastModified); + } + }); + } + }, less.poll); +} else { + less.optimization = 3; +} + +var cache; + +try { + cache = (typeof(window.localStorage) === 'undefined') ? null : window.localStorage; +} catch (_) { + cache = null; +} + +// +// Get all tags with the 'rel' attribute set to "stylesheet/less" +// +var links = document.getElementsByTagName('link'); +var typePattern = /^text\/(x-)?less$/; + +less.sheets = []; + +for (var i = 0; i < links.length; i++) { + if (links[i].rel === 'stylesheet/less' || (links[i].rel.match(/stylesheet/) && + (links[i].type.match(typePattern)))) { + less.sheets.push(links[i]); + } +} + + +less.refresh = function (reload) { + var startTime, endTime; + startTime = endTime = new(Date); + + loadStyleSheets(function (e, root, _, sheet, env) { + if (env.local) { + log("loading " + sheet.href + " from cache."); + } else { + log("parsed " + sheet.href + " successfully."); + createCSS(root.toCSS(), sheet, env.lastModified); + } + log("css for " + sheet.href + " generated in " + (new(Date) - endTime) + 'ms'); + (env.remaining === 0) && log("css generated in " + (new(Date) - startTime) + 'ms'); + endTime = new(Date); + }, reload); + + loadStyles(); +}; +less.refreshStyles = loadStyles; + +less.refresh(less.env === 'development'); + +function loadStyles() { + var styles = document.getElementsByTagName('style'); + for (var i = 0; i < styles.length; i++) { + if (styles[i].type.match(typePattern)) { + new(less.Parser)().parse(styles[i].innerHTML || '', function (e, tree) { + var css = tree.toCSS(); + var style = styles[i]; + style.type = 'text/css'; + if (style.styleSheet) { + style.styleSheet.cssText = css; + } else { + style.innerHTML = css; + } + }); + } + } +} + +function loadStyleSheets(callback, reload) { + for (var i = 0; i < less.sheets.length; i++) { + loadStyleSheet(less.sheets[i], callback, reload, less.sheets.length - (i + 1)); + } +} + +function loadStyleSheet(sheet, callback, reload, remaining) { + var url = window.location.href.replace(/[#?].*$/, ''); + var href = sheet.href.replace(/\?.*$/, ''); + var css = cache && cache.getItem(href); + var timestamp = cache && cache.getItem(href + ':timestamp'); + var styles = { css: css, timestamp: timestamp }; + + // Stylesheets in IE don't always return the full path + if (! /^(https?|file):/.test(href)) { + if (href.charAt(0) == "/") { + href = window.location.protocol + "//" + window.location.host + href; + } else { + href = url.slice(0, url.lastIndexOf('/') + 1) + href; + } + } + var filename = href.match(/([^\/]+)$/)[1]; + + xhr(sheet.href, sheet.type, function (data, lastModified) { + if (!reload && styles && lastModified && + (new(Date)(lastModified).valueOf() === + new(Date)(styles.timestamp).valueOf())) { + // Use local copy + createCSS(styles.css, sheet); + callback(null, null, data, sheet, { local: true, remaining: remaining }); + } else { + // Use remote copy (re-parse) + try { + new(less.Parser)({ + optimization: less.optimization, + paths: [href.replace(/[\w\.-]+$/, '')], + mime: sheet.type, + filename: filename + }).parse(data, function (e, root) { + if (e) { return error(e, href) } + try { + callback(e, root, data, sheet, { local: false, lastModified: lastModified, remaining: remaining }); + removeNode(document.getElementById('less-error-message:' + extractId(href))); + } catch (e) { + error(e, href); + } + }); + } catch (e) { + error(e, href); + } + } + }, function (status, url) { + throw new(Error)("Couldn't load " + url + " (" + status + ")"); + }); +} + +function extractId(href) { + return href.replace(/^[a-z]+:\/\/?[^\/]+/, '' ) // Remove protocol & domain + .replace(/^\//, '' ) // Remove root / + .replace(/\?.*$/, '' ) // Remove query + .replace(/\.[^\.\/]+$/, '' ) // Remove file extension + .replace(/[^\.\w-]+/g, '-') // Replace illegal characters + .replace(/\./g, ':'); // Replace dots with colons(for valid id) +} + +function createCSS(styles, sheet, lastModified) { + var css; + + // Strip the query-string + var href = sheet.href ? sheet.href.replace(/\?.*$/, '') : ''; + + // If there is no title set, use the filename, minus the extension + var id = 'less:' + (sheet.title || extractId(href)); + + // If the stylesheet doesn't exist, create a new node + if ((css = document.getElementById(id)) === null) { + css = document.createElement('style'); + css.type = 'text/css'; + css.media = sheet.media || 'screen'; + css.id = id; + document.getElementsByTagName('head')[0].appendChild(css); + } + + if (css.styleSheet) { // IE + try { + css.styleSheet.cssText = styles; + } catch (e) { + throw new(Error)("Couldn't reassign styleSheet.cssText."); + } + } else { + (function (node) { + if (css.childNodes.length > 0) { + if (css.firstChild.nodeValue !== node.nodeValue) { + css.replaceChild(node, css.firstChild); + } + } else { + css.appendChild(node); + } + })(document.createTextNode(styles)); + } + + // Don't update the local store if the file wasn't modified + if (lastModified && cache) { + log('saving ' + href + ' to cache.'); + cache.setItem(href, styles); + cache.setItem(href + ':timestamp', lastModified); + } +} + +function xhr(url, type, callback, errback) { + var xhr = getXMLHttpRequest(); + var async = isFileProtocol ? false : less.async; + + if (typeof(xhr.overrideMimeType) === 'function') { + xhr.overrideMimeType('text/css'); + } + xhr.open('GET', url, async); + xhr.setRequestHeader('Accept', type || 'text/x-less, text/css; q=0.9, */*; q=0.5'); + xhr.send(null); + + if (isFileProtocol) { + if (xhr.status === 0 || (xhr.status >= 200 && xhr.status < 300)) { + callback(xhr.responseText); + } else { + errback(xhr.status, url); + } + } else if (async) { + xhr.onreadystatechange = function () { + if (xhr.readyState == 4) { + handleResponse(xhr, callback, errback); + } + }; + } else { + handleResponse(xhr, callback, errback); + } + + function handleResponse(xhr, callback, errback) { + if (xhr.status >= 200 && xhr.status < 300) { + callback(xhr.responseText, + xhr.getResponseHeader("Last-Modified")); + } else if (typeof(errback) === 'function') { + errback(xhr.status, url); + } + } +} + +function getXMLHttpRequest() { + if (window.XMLHttpRequest) { + return new(XMLHttpRequest); + } else { + try { + return new(ActiveXObject)("MSXML2.XMLHTTP.3.0"); + } catch (e) { + log("browser doesn't support AJAX."); + return null; + } + } +} + +function removeNode(node) { + return node && node.parentNode.removeChild(node); +} + +function log(str) { + if (less.env == 'development' && typeof(console) !== "undefined") { console.log('less: ' + str) } +} + +function error(e, href) { + var id = 'less-error-message:' + extractId(href); + var template = '
  • {content}
  • '; + var elem = document.createElement('div'), timer, content, error = []; + var filename = e.filename || href; + + elem.id = id; + elem.className = "less-error-message"; + + content = '

    ' + (e.message || 'There is an error in your .less file') + + '

    ' + '

    in ' + filename + " "; + + var errorline = function (e, i, classname) { + if (e.extract[i]) { + error.push(template.replace(/\{line\}/, parseInt(e.line) + (i - 1)) + .replace(/\{class\}/, classname) + .replace(/\{content\}/, e.extract[i])); + } + }; + + if (e.stack) { + content += '
    ' + e.stack.split('\n').slice(1).join('
    '); + } else if (e.extract) { + errorline(e, 0, ''); + errorline(e, 1, 'line'); + errorline(e, 2, ''); + content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':

    ' + + '
      ' + error.join('') + '
    '; + } + elem.innerHTML = content; + + // CSS for error messages + createCSS([ + '.less-error-message ul, .less-error-message li {', + 'list-style-type: none;', + 'margin-right: 15px;', + 'padding: 4px 0;', + 'margin: 0;', + '}', + '.less-error-message label {', + 'font-size: 12px;', + 'margin-right: 15px;', + 'padding: 4px 0;', + 'color: #cc7777;', + '}', + '.less-error-message pre {', + 'color: #dd6666;', + 'padding: 4px 0;', + 'margin: 0;', + 'display: inline-block;', + '}', + '.less-error-message pre.line {', + 'color: #ff0000;', + '}', + '.less-error-message h3 {', + 'font-size: 20px;', + 'font-weight: bold;', + 'padding: 15px 0 5px 0;', + 'margin: 0;', + '}', + '.less-error-message a {', + 'color: #10a', + '}', + '.less-error-message .error {', + 'color: red;', + 'font-weight: bold;', + 'padding-bottom: 2px;', + 'border-bottom: 1px dashed red;', + '}' + ].join('\n'), { title: 'error-message' }); + + elem.style.cssText = [ + "font-family: Arial, sans-serif", + "border: 1px solid #e00", + "background-color: #eee", + "border-radius: 5px", + "-webkit-border-radius: 5px", + "-moz-border-radius: 5px", + "color: #e00", + "padding: 15px", + "margin-bottom: 15px" + ].join(';'); + + if (less.env == 'development') { + timer = setInterval(function () { + if (document.body) { + if (document.getElementById(id)) { + document.body.replaceChild(elem, document.getElementById(id)); + } else { + document.body.insertBefore(elem, document.body.firstChild); + } + clearInterval(timer); + } + }, 10); + } +} + diff --git a/dashboard/bin/lib/less/colors.js b/dashboard/bin/lib/less/colors.js new file mode 100644 index 00000000..ed4c2838 --- /dev/null +++ b/dashboard/bin/lib/less/colors.js @@ -0,0 +1,152 @@ +(function (tree) { + tree.colors = { + 'aliceblue':'#f0f8ff', + 'antiquewhite':'#faebd7', + 'aqua':'#00ffff', + 'aquamarine':'#7fffd4', + 'azure':'#f0ffff', + 'beige':'#f5f5dc', + 'bisque':'#ffe4c4', + 'black':'#000000', + 'blanchedalmond':'#ffebcd', + 'blue':'#0000ff', + 'blueviolet':'#8a2be2', + 'brown':'#a52a2a', + 'burlywood':'#deb887', + 'cadetblue':'#5f9ea0', + 'chartreuse':'#7fff00', + 'chocolate':'#d2691e', + 'coral':'#ff7f50', + 'cornflowerblue':'#6495ed', + 'cornsilk':'#fff8dc', + 'crimson':'#dc143c', + 'cyan':'#00ffff', + 'darkblue':'#00008b', + 'darkcyan':'#008b8b', + 'darkgoldenrod':'#b8860b', + 'darkgray':'#a9a9a9', + 'darkgrey':'#a9a9a9', + 'darkgreen':'#006400', + 'darkkhaki':'#bdb76b', + 'darkmagenta':'#8b008b', + 'darkolivegreen':'#556b2f', + 'darkorange':'#ff8c00', + 'darkorchid':'#9932cc', + 'darkred':'#8b0000', + 'darksalmon':'#e9967a', + 'darkseagreen':'#8fbc8f', + 'darkslateblue':'#483d8b', + 'darkslategray':'#2f4f4f', + 'darkslategrey':'#2f4f4f', + 'darkturquoise':'#00ced1', + 'darkviolet':'#9400d3', + 'deeppink':'#ff1493', + 'deepskyblue':'#00bfff', + 'dimgray':'#696969', + 'dimgrey':'#696969', + 'dodgerblue':'#1e90ff', + 'firebrick':'#b22222', + 'floralwhite':'#fffaf0', + 'forestgreen':'#228b22', + 'fuchsia':'#ff00ff', + 'gainsboro':'#dcdcdc', + 'ghostwhite':'#f8f8ff', + 'gold':'#ffd700', + 'goldenrod':'#daa520', + 'gray':'#808080', + 'grey':'#808080', + 'green':'#008000', + 'greenyellow':'#adff2f', + 'honeydew':'#f0fff0', + 'hotpink':'#ff69b4', + 'indianred':'#cd5c5c', + 'indigo':'#4b0082', + 'ivory':'#fffff0', + 'khaki':'#f0e68c', + 'lavender':'#e6e6fa', + 'lavenderblush':'#fff0f5', + 'lawngreen':'#7cfc00', + 'lemonchiffon':'#fffacd', + 'lightblue':'#add8e6', + 'lightcoral':'#f08080', + 'lightcyan':'#e0ffff', + 'lightgoldenrodyellow':'#fafad2', + 'lightgray':'#d3d3d3', + 'lightgrey':'#d3d3d3', + 'lightgreen':'#90ee90', + 'lightpink':'#ffb6c1', + 'lightsalmon':'#ffa07a', + 'lightseagreen':'#20b2aa', + 'lightskyblue':'#87cefa', + 'lightslategray':'#778899', + 'lightslategrey':'#778899', + 'lightsteelblue':'#b0c4de', + 'lightyellow':'#ffffe0', + 'lime':'#00ff00', + 'limegreen':'#32cd32', + 'linen':'#faf0e6', + 'magenta':'#ff00ff', + 'maroon':'#800000', + 'mediumaquamarine':'#66cdaa', + 'mediumblue':'#0000cd', + 'mediumorchid':'#ba55d3', + 'mediumpurple':'#9370d8', + 'mediumseagreen':'#3cb371', + 'mediumslateblue':'#7b68ee', + 'mediumspringgreen':'#00fa9a', + 'mediumturquoise':'#48d1cc', + 'mediumvioletred':'#c71585', + 'midnightblue':'#191970', + 'mintcream':'#f5fffa', + 'mistyrose':'#ffe4e1', + 'moccasin':'#ffe4b5', + 'navajowhite':'#ffdead', + 'navy':'#000080', + 'oldlace':'#fdf5e6', + 'olive':'#808000', + 'olivedrab':'#6b8e23', + 'orange':'#ffa500', + 'orangered':'#ff4500', + 'orchid':'#da70d6', + 'palegoldenrod':'#eee8aa', + 'palegreen':'#98fb98', + 'paleturquoise':'#afeeee', + 'palevioletred':'#d87093', + 'papayawhip':'#ffefd5', + 'peachpuff':'#ffdab9', + 'peru':'#cd853f', + 'pink':'#ffc0cb', + 'plum':'#dda0dd', + 'powderblue':'#b0e0e6', + 'purple':'#800080', + 'red':'#ff0000', + 'rosybrown':'#bc8f8f', + 'royalblue':'#4169e1', + 'saddlebrown':'#8b4513', + 'salmon':'#fa8072', + 'sandybrown':'#f4a460', + 'seagreen':'#2e8b57', + 'seashell':'#fff5ee', + 'sienna':'#a0522d', + 'silver':'#c0c0c0', + 'skyblue':'#87ceeb', + 'slateblue':'#6a5acd', + 'slategray':'#708090', + 'slategrey':'#708090', + 'snow':'#fffafa', + 'springgreen':'#00ff7f', + 'steelblue':'#4682b4', + 'tan':'#d2b48c', + 'teal':'#008080', + 'thistle':'#d8bfd8', + 'tomato':'#ff6347', + 'transparent':'rgba(0,0,0,0)', + 'turquoise':'#40e0d0', + 'violet':'#ee82ee', + 'wheat':'#f5deb3', + 'white':'#ffffff', + 'whitesmoke':'#f5f5f5', + 'yellow':'#ffff00', + 'yellowgreen':'#9acd32' + }; +})(require('./tree')); diff --git a/dashboard/bin/lib/less/cssmin.js b/dashboard/bin/lib/less/cssmin.js new file mode 100644 index 00000000..427de71c --- /dev/null +++ b/dashboard/bin/lib/less/cssmin.js @@ -0,0 +1,355 @@ +/** + * cssmin.js + * Author: Stoyan Stefanov - http://phpied.com/ + * This is a JavaScript port of the CSS minification tool + * distributed with YUICompressor, itself a port + * of the cssmin utility by Isaac Schlueter - http://foohack.com/ + * Permission is hereby granted to use the JavaScript version under the same + * conditions as the YUICompressor (original YUICompressor note below). + */ + +/* +* YUI Compressor +* http://developer.yahoo.com/yui/compressor/ +* Author: Julien Lecomte - http://www.julienlecomte.net/ +* Copyright (c) 2011 Yahoo! Inc. All rights reserved. +* The copyrights embodied in the content of this file are licensed +* by Yahoo! Inc. under the BSD (revised) open source license. +*/ +var YAHOO = YAHOO || {}; +YAHOO.compressor = YAHOO.compressor || {}; + +/** + * Utility method to replace all data urls with tokens before we start + * compressing, to avoid performance issues running some of the subsequent + * regexes against large strings chunks. + * + * @private + * @method _extractDataUrls + * @param {String} css The input css + * @param {Array} The global array of tokens to preserve + * @returns String The processed css + */ +YAHOO.compressor._extractDataUrls = function (css, preservedTokens) { + + // Leave data urls alone to increase parse performance. + var maxIndex = css.length - 1, + appendIndex = 0, + startIndex, + endIndex, + terminator, + foundTerminator, + sb = [], + m, + preserver, + token, + pattern = /url\(\s*(["']?)data\:/g; + + // Since we need to account for non-base64 data urls, we need to handle + // ' and ) being part of the data string. Hence switching to indexOf, + // to determine whether or not we have matching string terminators and + // handling sb appends directly, instead of using matcher.append* methods. + + while ((m = pattern.exec(css)) !== null) { + + startIndex = m.index + 4; // "url(".length() + terminator = m[1]; // ', " or empty (not quoted) + + if (terminator.length === 0) { + terminator = ")"; + } + + foundTerminator = false; + + endIndex = pattern.lastIndex - 1; + + while(foundTerminator === false && endIndex+1 <= maxIndex) { + endIndex = css.indexOf(terminator, endIndex + 1); + + // endIndex == 0 doesn't really apply here + if ((endIndex > 0) && (css.charAt(endIndex - 1) !== '\\')) { + foundTerminator = true; + if (")" != terminator) { + endIndex = css.indexOf(")", endIndex); + } + } + } + + // Enough searching, start moving stuff over to the buffer + sb.push(css.substring(appendIndex, m.index)); + + if (foundTerminator) { + token = css.substring(startIndex, endIndex); + token = token.replace(/\s+/g, ""); + preservedTokens.push(token); + + preserver = "url(___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___)"; + sb.push(preserver); + + appendIndex = endIndex + 1; + } else { + // No end terminator found, re-add the whole match. Should we throw/warn here? + sb.push(css.substring(m.index, pattern.lastIndex)); + appendIndex = pattern.lastIndex; + } + } + + sb.push(css.substring(appendIndex)); + + return sb.join(""); +}; + +/** + * Utility method to compress hex color values of the form #AABBCC to #ABC. + * + * DOES NOT compress CSS ID selectors which match the above pattern (which would break things). + * e.g. #AddressForm { ... } + * + * DOES NOT compress IE filters, which have hex color values (which would break things). + * e.g. filter: chroma(color="#FFFFFF"); + * + * DOES NOT compress invalid hex values. + * e.g. background-color: #aabbccdd + * + * @private + * @method _compressHexColors + * @param {String} css The input css + * @returns String The processed css + */ +YAHOO.compressor._compressHexColors = function(css) { + + // Look for hex colors inside { ... } (to avoid IDs) and which don't have a =, or a " in front of them (to avoid filters) + var pattern = /(\=\s*?["']?)?#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])(\}|[^0-9a-f{][^{]*?\})/gi, + m, + index = 0, + isFilter, + sb = []; + + while ((m = pattern.exec(css)) !== null) { + + sb.push(css.substring(index, m.index)); + + isFilter = m[1]; + + if (isFilter) { + // Restore, maintain case, otherwise filter will break + sb.push(m[1] + "#" + (m[2] + m[3] + m[4] + m[5] + m[6] + m[7])); + } else { + if (m[2].toLowerCase() == m[3].toLowerCase() && + m[4].toLowerCase() == m[5].toLowerCase() && + m[6].toLowerCase() == m[7].toLowerCase()) { + + // Compress. + sb.push("#" + (m[3] + m[5] + m[7]).toLowerCase()); + } else { + // Non compressible color, restore but lower case. + sb.push("#" + (m[2] + m[3] + m[4] + m[5] + m[6] + m[7]).toLowerCase()); + } + } + + index = pattern.lastIndex = pattern.lastIndex - m[8].length; + } + + sb.push(css.substring(index)); + + return sb.join(""); +}; + +YAHOO.compressor.cssmin = function (css, linebreakpos) { + + var startIndex = 0, + endIndex = 0, + i = 0, max = 0, + preservedTokens = [], + comments = [], + token = '', + totallen = css.length, + placeholder = ''; + + css = this._extractDataUrls(css, preservedTokens); + + // collect all comment blocks... + while ((startIndex = css.indexOf("/*", startIndex)) >= 0) { + endIndex = css.indexOf("*/", startIndex + 2); + if (endIndex < 0) { + endIndex = totallen; + } + token = css.slice(startIndex + 2, endIndex); + comments.push(token); + css = css.slice(0, startIndex + 2) + "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.length - 1) + "___" + css.slice(endIndex); + startIndex += 2; + } + + // preserve strings so their content doesn't get accidentally minified + css = css.replace(/("([^\\"]|\\.|\\)*")|('([^\\']|\\.|\\)*')/g, function (match) { + var i, max, quote = match.substring(0, 1); + + match = match.slice(1, -1); + + // maybe the string contains a comment-like substring? + // one, maybe more? put'em back then + if (match.indexOf("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_") >= 0) { + for (i = 0, max = comments.length; i < max; i = i + 1) { + match = match.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments[i]); + } + } + + // minify alpha opacity in filter strings + match = match.replace(/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/gi, "alpha(opacity="); + + preservedTokens.push(match); + return quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___" + quote; + }); + + // strings are safe, now wrestle the comments + for (i = 0, max = comments.length; i < max; i = i + 1) { + + token = comments[i]; + placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___"; + + // ! in the first position of the comment means preserve + // so push to the preserved tokens keeping the ! + if (token.charAt(0) === "!") { + preservedTokens.push(token); + css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); + continue; + } + + // \ in the last position looks like hack for Mac/IE5 + // shorten that to /*\*/ and the next one to /**/ + if (token.charAt(token.length - 1) === "\\") { + preservedTokens.push("\\"); + css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); + i = i + 1; // attn: advancing the loop + preservedTokens.push(""); + css = css.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); + continue; + } + + // keep empty comments after child selectors (IE7 hack) + // e.g. html >/**/ body + if (token.length === 0) { + startIndex = css.indexOf(placeholder); + if (startIndex > 2) { + if (css.charAt(startIndex - 3) === '>') { + preservedTokens.push(""); + css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); + } + } + } + + // in all other cases kill the comment + css = css.replace("/*" + placeholder + "*/", ""); + } + + + // Normalize all whitespace strings to single spaces. Easier to work with that way. + css = css.replace(/\s+/g, " "); + + // Remove the spaces before the things that should not have spaces before them. + // But, be careful not to turn "p :link {...}" into "p:link{...}" + // Swap out any pseudo-class colons with the token, and then swap back. + css = css.replace(/(^|\})(([^\{:])+:)+([^\{]*\{)/g, function (m) { + return m.replace(":", "___YUICSSMIN_PSEUDOCLASSCOLON___"); + }); + css = css.replace(/\s+([!{};:>+\(\)\],])/g, '$1'); + css = css.replace(/___YUICSSMIN_PSEUDOCLASSCOLON___/g, ":"); + + // retain space for special IE6 cases + css = css.replace(/:first-(line|letter)(\{|,)/g, ":first-$1 $2"); + + // no space after the end of a preserved comment + css = css.replace(/\*\/ /g, '*/'); + + + // If there is a @charset, then only allow one, and push to the top of the file. + css = css.replace(/^(.*)(@charset "[^"]*";)/gi, '$2$1'); + css = css.replace(/^(\s*@charset [^;]+;\s*)+/gi, '$1'); + + // Put the space back in some cases, to support stuff like + // @media screen and (-webkit-min-device-pixel-ratio:0){ + css = css.replace(/\band\(/gi, "and ("); + + + // Remove the spaces after the things that should not have spaces after them. + css = css.replace(/([!{}:;>+\(\[,])\s+/g, '$1'); + + // remove unnecessary semicolons + css = css.replace(/;+\}/g, "}"); + + // Replace 0(px,em,%) with 0. + css = css.replace(/([\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)/gi, "$1$2"); + + // Replace 0 0 0 0; with 0. + css = css.replace(/:0 0 0 0(;|\})/g, ":0$1"); + css = css.replace(/:0 0 0(;|\})/g, ":0$1"); + css = css.replace(/:0 0(;|\})/g, ":0$1"); + + // Replace background-position:0; with background-position:0 0; + // same for transform-origin + css = css.replace(/(background-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin):0(;|\})/gi, function(all, prop, tail) { + return prop.toLowerCase() + ":0 0" + tail; + }); + + // Replace 0.6 to .6, but only when preceded by : or a white-space + css = css.replace(/(:|\s)0+\.(\d+)/g, "$1.$2"); + + // Shorten colors from rgb(51,102,153) to #336699 + // This makes it more likely that it'll get further compressed in the next step. + css = css.replace(/rgb\s*\(\s*([0-9,\s]+)\s*\)/gi, function () { + var i, rgbcolors = arguments[1].split(','); + for (i = 0; i < rgbcolors.length; i = i + 1) { + rgbcolors[i] = parseInt(rgbcolors[i], 10).toString(16); + if (rgbcolors[i].length === 1) { + rgbcolors[i] = '0' + rgbcolors[i]; + } + } + return '#' + rgbcolors.join(''); + }); + + // Shorten colors from #AABBCC to #ABC. + css = this._compressHexColors(css); + + // border: none -> border:0 + css = css.replace(/(border|border-top|border-right|border-bottom|border-right|outline|background):none(;|\})/gi, function(all, prop, tail) { + return prop.toLowerCase() + ":0" + tail; + }); + + // shorter opacity IE filter + css = css.replace(/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/gi, "alpha(opacity="); + + // Remove empty rules. + css = css.replace(/[^\};\{\/]+\{\}/g, ""); + + if (linebreakpos >= 0) { + // Some source control tools don't like it when files containing lines longer + // than, say 8000 characters, are checked in. The linebreak option is used in + // that case to split long lines after a specific column. + startIndex = 0; + i = 0; + while (i < css.length) { + i = i + 1; + if (css[i - 1] === '}' && i - startIndex > linebreakpos) { + css = css.slice(0, i) + '\n' + css.slice(i); + startIndex = i; + } + } + } + + // Replace multiple semi-colons in a row by a single one + // See SF bug #1980989 + css = css.replace(/;;+/g, ";"); + + // restore preserved comments and strings + for (i = 0, max = preservedTokens.length; i < max; i = i + 1) { + css = css.replace("___YUICSSMIN_PRESERVED_TOKEN_" + i + "___", preservedTokens[i]); + } + + // Trim the final string (for any leading or trailing white spaces) + css = css.replace(/^\s+|\s+$/g, ""); + + return css; + +}; + +exports.compressor = YAHOO.compressor; diff --git a/dashboard/bin/lib/less/functions.js b/dashboard/bin/lib/less/functions.js new file mode 100644 index 00000000..6eb34bac --- /dev/null +++ b/dashboard/bin/lib/less/functions.js @@ -0,0 +1,228 @@ +(function (tree) { + +tree.functions = { + rgb: function (r, g, b) { + return this.rgba(r, g, b, 1.0); + }, + rgba: function (r, g, b, a) { + var rgb = [r, g, b].map(function (c) { return number(c) }), + a = number(a); + return new(tree.Color)(rgb, a); + }, + hsl: function (h, s, l) { + return this.hsla(h, s, l, 1.0); + }, + hsla: function (h, s, l, a) { + h = (number(h) % 360) / 360; + s = number(s); l = number(l); a = number(a); + + var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s; + var m1 = l * 2 - m2; + + return this.rgba(hue(h + 1/3) * 255, + hue(h) * 255, + hue(h - 1/3) * 255, + a); + + function hue(h) { + h = h < 0 ? h + 1 : (h > 1 ? h - 1 : h); + if (h * 6 < 1) return m1 + (m2 - m1) * h * 6; + else if (h * 2 < 1) return m2; + else if (h * 3 < 2) return m1 + (m2 - m1) * (2/3 - h) * 6; + else return m1; + } + }, + hue: function (color) { + return new(tree.Dimension)(Math.round(color.toHSL().h)); + }, + saturation: function (color) { + return new(tree.Dimension)(Math.round(color.toHSL().s * 100), '%'); + }, + lightness: function (color) { + return new(tree.Dimension)(Math.round(color.toHSL().l * 100), '%'); + }, + alpha: function (color) { + return new(tree.Dimension)(color.toHSL().a); + }, + saturate: function (color, amount) { + var hsl = color.toHSL(); + + hsl.s += amount.value / 100; + hsl.s = clamp(hsl.s); + return hsla(hsl); + }, + desaturate: function (color, amount) { + var hsl = color.toHSL(); + + hsl.s -= amount.value / 100; + hsl.s = clamp(hsl.s); + return hsla(hsl); + }, + lighten: function (color, amount) { + var hsl = color.toHSL(); + + hsl.l += amount.value / 100; + hsl.l = clamp(hsl.l); + return hsla(hsl); + }, + darken: function (color, amount) { + var hsl = color.toHSL(); + + hsl.l -= amount.value / 100; + hsl.l = clamp(hsl.l); + return hsla(hsl); + }, + fadein: function (color, amount) { + var hsl = color.toHSL(); + + hsl.a += amount.value / 100; + hsl.a = clamp(hsl.a); + return hsla(hsl); + }, + fadeout: function (color, amount) { + var hsl = color.toHSL(); + + hsl.a -= amount.value / 100; + hsl.a = clamp(hsl.a); + return hsla(hsl); + }, + fade: function (color, amount) { + var hsl = color.toHSL(); + + hsl.a = amount.value / 100; + hsl.a = clamp(hsl.a); + return hsla(hsl); + }, + spin: function (color, amount) { + var hsl = color.toHSL(); + var hue = (hsl.h + amount.value) % 360; + + hsl.h = hue < 0 ? 360 + hue : hue; + + return hsla(hsl); + }, + // + // Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein + // http://sass-lang.com + // + mix: function (color1, color2, weight) { + var p = weight.value / 100.0; + var w = p * 2 - 1; + var a = color1.toHSL().a - color2.toHSL().a; + + var w1 = (((w * a == -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0; + var w2 = 1 - w1; + + var rgb = [color1.rgb[0] * w1 + color2.rgb[0] * w2, + color1.rgb[1] * w1 + color2.rgb[1] * w2, + color1.rgb[2] * w1 + color2.rgb[2] * w2]; + + var alpha = color1.alpha * p + color2.alpha * (1 - p); + + return new(tree.Color)(rgb, alpha); + }, + greyscale: function (color) { + return this.desaturate(color, new(tree.Dimension)(100)); + }, + e: function (str) { + return new(tree.Anonymous)(str instanceof tree.JavaScript ? str.evaluated : str); + }, + escape: function (str) { + return new(tree.Anonymous)(encodeURI(str.value).replace(/=/g, "%3D").replace(/:/g, "%3A").replace(/#/g, "%23").replace(/;/g, "%3B").replace(/\(/g, "%28").replace(/\)/g, "%29")); + }, + '%': function (quoted /* arg, arg, ...*/) { + var args = Array.prototype.slice.call(arguments, 1), + str = quoted.value; + + for (var i = 0; i < args.length; i++) { + str = str.replace(/%[sda]/i, function(token) { + var value = token.match(/s/i) ? args[i].value : args[i].toCSS(); + return token.match(/[A-Z]$/) ? encodeURIComponent(value) : value; + }); + } + str = str.replace(/%%/g, '%'); + return new(tree.Quoted)('"' + str + '"', str); + }, + round: function (n) { + return this._math('round', n); + }, + ceil: function (n) { + return this._math('ceil', n); + }, + floor: function (n) { + return this._math('floor', n); + }, + _math: function (fn, n) { + if (n instanceof tree.Dimension) { + return new(tree.Dimension)(Math[fn](number(n)), n.unit); + } else if (typeof(n) === 'number') { + return Math[fn](n); + } else { + throw { type: "Argument", message: "argument must be a number" }; + } + }, + argb: function (color) { + return new(tree.Anonymous)(color.toARGB()); + + }, + percentage: function (n) { + return new(tree.Dimension)(n.value * 100, '%'); + }, + color: function (n) { + if (n instanceof tree.Quoted) { + return new(tree.Color)(n.value.slice(1)); + } else { + throw { type: "Argument", message: "argument must be a string" }; + } + }, + iscolor: function (n) { + return this._isa(n, tree.Color); + }, + isnumber: function (n) { + return this._isa(n, tree.Dimension); + }, + isstring: function (n) { + return this._isa(n, tree.Quoted); + }, + iskeyword: function (n) { + return this._isa(n, tree.Keyword); + }, + isurl: function (n) { + return this._isa(n, tree.URL); + }, + ispixel: function (n) { + return (n instanceof tree.Dimension) && n.unit === 'px' ? tree.True : tree.False; + }, + ispercentage: function (n) { + return (n instanceof tree.Dimension) && n.unit === '%' ? tree.True : tree.False; + }, + isem: function (n) { + return (n instanceof tree.Dimension) && n.unit === 'em' ? tree.True : tree.False; + }, + _isa: function (n, Type) { + return (n instanceof Type) ? tree.True : tree.False; + } +}; + +function hsla(hsla) { + return tree.functions.hsla(hsla.h, hsla.s, hsla.l, hsla.a); +} + +function number(n) { + if (n instanceof tree.Dimension) { + return parseFloat(n.unit == '%' ? n.value / 100 : n.value); + } else if (typeof(n) === 'number') { + return n; + } else { + throw { + error: "RuntimeError", + message: "color functions take numbers as parameters" + }; + } +} + +function clamp(val) { + return Math.min(1, Math.max(0, val)); +} + +})(require('./tree')); diff --git a/dashboard/bin/lib/less/index.js b/dashboard/bin/lib/less/index.js new file mode 100644 index 00000000..a11fa998 --- /dev/null +++ b/dashboard/bin/lib/less/index.js @@ -0,0 +1,148 @@ +var path = require('path'), + sys = require('util'), + fs = require('fs'); + +var less = { + version: [1, 3, 0], + Parser: require('./parser').Parser, + importer: require('./parser').importer, + tree: require('./tree'), + render: function (input, options, callback) { + options = options || {}; + + if (typeof(options) === 'function') { + callback = options, options = {}; + } + + var parser = new(less.Parser)(options), + ee; + + if (callback) { + parser.parse(input, function (e, root) { + callback(e, root && root.toCSS && root.toCSS(options)); + }); + } else { + ee = new(require('events').EventEmitter); + + process.nextTick(function () { + parser.parse(input, function (e, root) { + if (e) { ee.emit('error', e) } + else { ee.emit('success', root.toCSS(options)) } + }); + }); + return ee; + } + }, + writeError: function (ctx, options) { + options = options || {}; + + var message = ""; + var extract = ctx.extract; + var error = []; + var stylize = options.color ? less.stylize : function (str) { return str }; + + if (options.silent) { return } + + if (ctx.stack) { return sys.error(stylize(ctx.stack, 'red')) } + + if (!ctx.hasOwnProperty('index')) { + return sys.error(ctx.stack || ctx.message); + } + + if (typeof(extract[0]) === 'string') { + error.push(stylize((ctx.line - 1) + ' ' + extract[0], 'grey')); + } + + if (extract[1]) { + error.push(ctx.line + ' ' + extract[1].slice(0, ctx.column) + + stylize(stylize(stylize(extract[1][ctx.column], 'bold') + + extract[1].slice(ctx.column + 1), 'red'), 'inverse')); + } + + if (typeof(extract[2]) === 'string') { + error.push(stylize((ctx.line + 1) + ' ' + extract[2], 'grey')); + } + error = error.join('\n') + '\033[0m\n'; + + message += stylize(ctx.type + 'Error: ' + ctx.message, 'red'); + ctx.filename && (message += stylize(' in ', 'red') + ctx.filename + + stylize(':' + ctx.line + ':' + ctx.column, 'grey')); + + sys.error(message, error); + + if (ctx.callLine) { + sys.error(stylize('from ', 'red') + (ctx.filename || '')); + sys.error(stylize(ctx.callLine, 'grey') + ' ' + ctx.callExtract); + } + } +}; + +['color', 'directive', 'operation', 'dimension', + 'keyword', 'variable', 'ruleset', 'element', + 'selector', 'quoted', 'expression', 'rule', + 'call', 'url', 'alpha', 'import', + 'mixin', 'comment', 'anonymous', 'value', + 'javascript', 'assignment', 'condition', 'paren', + 'media' +].forEach(function (n) { + require('./tree/' + n); +}); + +less.Parser.importer = function (file, paths, callback, env) { + var pathname; + + // TODO: Undo this at some point, + // or use different approach. + paths.unshift('.'); + + for (var i = 0; i < paths.length; i++) { + try { + pathname = path.join(paths[i], file); + fs.statSync(pathname); + break; + } catch (e) { + pathname = null; + } + } + + if (pathname) { + fs.readFile(pathname, 'utf-8', function(e, data) { + if (e) return callback(e); + + new(less.Parser)({ + paths: [path.dirname(pathname)].concat(paths), + filename: pathname + }).parse(data, function (e, root) { + callback(e, root, data); + }); + }); + } else { + if (typeof(env.errback) === "function") { + env.errback(file, paths, callback); + } else { + callback({ type: 'File', message: "'" + file + "' wasn't found.\n" }); + } + } +} + +require('./functions'); +require('./colors'); + +for (var k in less) { exports[k] = less[k] } + +// Stylize a string +function stylize(str, style) { + var styles = { + 'bold' : [1, 22], + 'inverse' : [7, 27], + 'underline' : [4, 24], + 'yellow' : [33, 39], + 'green' : [32, 39], + 'red' : [31, 39], + 'grey' : [90, 39] + }; + return '\033[' + styles[style][0] + 'm' + str + + '\033[' + styles[style][1] + 'm'; +} +less.stylize = stylize; + diff --git a/dashboard/bin/lib/less/parser.js b/dashboard/bin/lib/less/parser.js new file mode 100644 index 00000000..d732e1b1 --- /dev/null +++ b/dashboard/bin/lib/less/parser.js @@ -0,0 +1,1334 @@ +var less, tree; + +if (typeof environment === "object" && ({}).toString.call(environment) === "[object Environment]") { + // Rhino + // Details on how to detect Rhino: https://github.com/ringo/ringojs/issues/88 + if (typeof(window) === 'undefined') { less = {} } + else { less = window.less = {} } + tree = less.tree = {}; + less.mode = 'rhino'; +} else if (typeof(window) === 'undefined') { + // Node.js + less = exports, + tree = require('./tree'); + less.mode = 'node'; +} else { + // Browser + if (typeof(window.less) === 'undefined') { window.less = {} } + less = window.less, + tree = window.less.tree = {}; + less.mode = 'browser'; +} +// +// less.js - parser +// +// A relatively straight-forward predictive parser. +// There is no tokenization/lexing stage, the input is parsed +// in one sweep. +// +// To make the parser fast enough to run in the browser, several +// optimization had to be made: +// +// - Matching and slicing on a huge input is often cause of slowdowns. +// The solution is to chunkify the input into smaller strings. +// The chunks are stored in the `chunks` var, +// `j` holds the current chunk index, and `current` holds +// the index of the current chunk in relation to `input`. +// This gives us an almost 4x speed-up. +// +// - In many cases, we don't need to match individual tokens; +// for example, if a value doesn't hold any variables, operations +// or dynamic references, the parser can effectively 'skip' it, +// treating it as a literal. +// An example would be '1px solid #000' - which evaluates to itself, +// we don't need to know what the individual components are. +// The drawback, of course is that you don't get the benefits of +// syntax-checking on the CSS. This gives us a 50% speed-up in the parser, +// and a smaller speed-up in the code-gen. +// +// +// Token matching is done with the `$` function, which either takes +// a terminal string or regexp, or a non-terminal function to call. +// It also takes care of moving all the indices forwards. +// +// +less.Parser = function Parser(env) { + var input, // LeSS input string + i, // current index in `input` + j, // current chunk + temp, // temporarily holds a chunk's state, for backtracking + memo, // temporarily holds `i`, when backtracking + furthest, // furthest index the parser has gone to + chunks, // chunkified input + current, // index of current chunk, in `input` + parser; + + var that = this; + + // This function is called after all files + // have been imported through `@import`. + var finish = function () {}; + + var imports = this.imports = { + paths: env && env.paths || [], // Search paths, when importing + queue: [], // Files which haven't been imported yet + files: {}, // Holds the imported parse trees + contents: {}, // Holds the imported file contents + mime: env && env.mime, // MIME type of .less files + error: null, // Error in parsing/evaluating an import + push: function (path, callback) { + var that = this; + this.queue.push(path); + + // + // Import a file asynchronously + // + less.Parser.importer(path, this.paths, function (e, root, contents) { + that.queue.splice(that.queue.indexOf(path), 1); // Remove the path from the queue + + var imported = path in that.files; + + that.files[path] = root; // Store the root + that.contents[path] = contents; + + if (e && !that.error) { that.error = e } + + callback(e, root, imported); + + if (that.queue.length === 0) { finish() } // Call `finish` if we're done importing + }, env); + } + }; + + function save() { temp = chunks[j], memo = i, current = i } + function restore() { chunks[j] = temp, i = memo, current = i } + + function sync() { + if (i > current) { + chunks[j] = chunks[j].slice(i - current); + current = i; + } + } + // + // Parse from a token, regexp or string, and move forward if match + // + function $(tok) { + var match, args, length, c, index, endIndex, k, mem; + + // + // Non-terminal + // + if (tok instanceof Function) { + return tok.call(parser.parsers); + // + // Terminal + // + // Either match a single character in the input, + // or match a regexp in the current chunk (chunk[j]). + // + } else if (typeof(tok) === 'string') { + match = input.charAt(i) === tok ? tok : null; + length = 1; + sync (); + } else { + sync (); + + if (match = tok.exec(chunks[j])) { + length = match[0].length; + } else { + return null; + } + } + + // The match is confirmed, add the match length to `i`, + // and consume any extra white-space characters (' ' || '\n') + // which come after that. The reason for this is that LeSS's + // grammar is mostly white-space insensitive. + // + if (match) { + mem = i += length; + endIndex = i + chunks[j].length - length; + + while (i < endIndex) { + c = input.charCodeAt(i); + if (! (c === 32 || c === 10 || c === 9)) { break } + i++; + } + chunks[j] = chunks[j].slice(length + (i - mem)); + current = i; + + if (chunks[j].length === 0 && j < chunks.length - 1) { j++ } + + if(typeof(match) === 'string') { + return match; + } else { + return match.length === 1 ? match[0] : match; + } + } + } + + function expect(arg, msg) { + var result = $(arg); + if (! result) { + error(msg || (typeof(arg) === 'string' ? "expected '" + arg + "' got '" + input.charAt(i) + "'" + : "unexpected token")); + } else { + return result; + } + } + + function error(msg, type) { + throw { index: i, type: type || 'Syntax', message: msg }; + } + + // Same as $(), but don't change the state of the parser, + // just return the match. + function peek(tok) { + if (typeof(tok) === 'string') { + return input.charAt(i) === tok; + } else { + if (tok.test(chunks[j])) { + return true; + } else { + return false; + } + } + } + + function basename(pathname) { + if (less.mode === 'node') { + return require('path').basename(pathname); + } else { + return pathname.match(/[^\/]+$/)[0]; + } + } + + function getInput(e, env) { + if (e.filename && env.filename && (e.filename !== env.filename)) { + return parser.imports.contents[basename(e.filename)]; + } else { + return input; + } + } + + function getLocation(index, input) { + for (var n = index, column = -1; + n >= 0 && input.charAt(n) !== '\n'; + n--) { column++ } + + return { line: typeof(index) === 'number' ? (input.slice(0, index).match(/\n/g) || "").length : null, + column: column }; + } + + function LessError(e, env) { + var input = getInput(e, env), + loc = getLocation(e.index, input), + line = loc.line, + col = loc.column, + lines = input.split('\n'); + + this.type = e.type || 'Syntax'; + this.message = e.message; + this.filename = e.filename || env.filename; + this.index = e.index; + this.line = typeof(line) === 'number' ? line + 1 : null; + this.callLine = e.call && (getLocation(e.call, input).line + 1); + this.callExtract = lines[getLocation(e.call, input).line]; + this.stack = e.stack; + this.column = col; + this.extract = [ + lines[line - 1], + lines[line], + lines[line + 1] + ]; + } + + this.env = env = env || {}; + + // The optimization level dictates the thoroughness of the parser, + // the lower the number, the less nodes it will create in the tree. + // This could matter for debugging, or if you want to access + // the individual nodes in the tree. + this.optimization = ('optimization' in this.env) ? this.env.optimization : 1; + + this.env.filename = this.env.filename || null; + + // + // The Parser + // + return parser = { + + imports: imports, + // + // Parse an input string into an abstract syntax tree, + // call `callback` when done. + // + parse: function (str, callback) { + var root, start, end, zone, line, lines, buff = [], c, error = null; + + i = j = current = furthest = 0; + input = str.replace(/\r\n/g, '\n'); + + // Split the input into chunks. + chunks = (function (chunks) { + var j = 0, + skip = /[^"'`\{\}\/\(\)\\]+/g, + comment = /\/\*(?:[^*]|\*+[^\/*])*\*+\/|\/\/.*/g, + string = /"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'|`((?:[^`\\\r\n]|\\.)*)`/g, + level = 0, + match, + chunk = chunks[0], + inParam; + + for (var i = 0, c, cc; i < input.length; i++) { + skip.lastIndex = i; + if (match = skip.exec(input)) { + if (match.index === i) { + i += match[0].length; + chunk.push(match[0]); + } + } + c = input.charAt(i); + comment.lastIndex = string.lastIndex = i; + + if (match = string.exec(input)) { + if (match.index === i) { + i += match[0].length; + chunk.push(match[0]); + c = input.charAt(i); + } + } + + if (!inParam && c === '/') { + cc = input.charAt(i + 1); + if (cc === '/' || cc === '*') { + if (match = comment.exec(input)) { + if (match.index === i) { + i += match[0].length; + chunk.push(match[0]); + c = input.charAt(i); + } + } + } + } + + switch (c) { + case '{': if (! inParam) { level ++; chunk.push(c); break } + case '}': if (! inParam) { level --; chunk.push(c); chunks[++j] = chunk = []; break } + case '(': if (! inParam) { inParam = true; chunk.push(c); break } + case ')': if ( inParam) { inParam = false; chunk.push(c); break } + default: chunk.push(c); + } + } + if (level > 0) { + error = new(LessError)({ + index: i, + type: 'Parse', + message: "missing closing `}`", + filename: env.filename + }, env); + } + + return chunks.map(function (c) { return c.join('') });; + })([[]]); + + if (error) { + return callback(error); + } + + // Start with the primary rule. + // The whole syntax tree is held under a Ruleset node, + // with the `root` property set to true, so no `{}` are + // output. The callback is called when the input is parsed. + try { + root = new(tree.Ruleset)([], $(this.parsers.primary)); + root.root = true; + } catch (e) { + return callback(new(LessError)(e, env)); + } + + root.toCSS = (function (evaluate) { + var line, lines, column; + + return function (options, variables) { + var frames = [], importError; + + options = options || {}; + // + // Allows setting variables with a hash, so: + // + // `{ color: new(tree.Color)('#f01') }` will become: + // + // new(tree.Rule)('@color', + // new(tree.Value)([ + // new(tree.Expression)([ + // new(tree.Color)('#f01') + // ]) + // ]) + // ) + // + if (typeof(variables) === 'object' && !Array.isArray(variables)) { + variables = Object.keys(variables).map(function (k) { + var value = variables[k]; + + if (! (value instanceof tree.Value)) { + if (! (value instanceof tree.Expression)) { + value = new(tree.Expression)([value]); + } + value = new(tree.Value)([value]); + } + return new(tree.Rule)('@' + k, value, false, 0); + }); + frames = [new(tree.Ruleset)(null, variables)]; + } + + try { + var css = evaluate.call(this, { frames: frames }) + .toCSS([], { compress: options.compress || false }); + } catch (e) { + throw new(LessError)(e, env); + } + + if ((importError = parser.imports.error)) { // Check if there was an error during importing + if (importError instanceof LessError) throw importError; + else throw new(LessError)(importError, env); + } + + if (options.yuicompress && less.mode === 'node') { + return require('./cssmin').compressor.cssmin(css); + } else if (options.compress) { + return css.replace(/(\s)+/g, "$1"); + } else { + return css; + } + }; + })(root.eval); + + // If `i` is smaller than the `input.length - 1`, + // it means the parser wasn't able to parse the whole + // string, so we've got a parsing error. + // + // We try to extract a \n delimited string, + // showing the line where the parse error occured. + // We split it up into two parts (the part which parsed, + // and the part which didn't), so we can color them differently. + if (i < input.length - 1) { + i = furthest; + lines = input.split('\n'); + line = (input.slice(0, i).match(/\n/g) || "").length + 1; + + for (var n = i, column = -1; n >= 0 && input.charAt(n) !== '\n'; n--) { column++ } + + error = { + type: "Parse", + message: "Syntax Error on line " + line, + index: i, + filename: env.filename, + line: line, + column: column, + extract: [ + lines[line - 2], + lines[line - 1], + lines[line] + ] + }; + } + + if (this.imports.queue.length > 0) { + finish = function () { callback(error, root) }; + } else { + callback(error, root); + } + }, + + // + // Here in, the parsing rules/functions + // + // The basic structure of the syntax tree generated is as follows: + // + // Ruleset -> Rule -> Value -> Expression -> Entity + // + // Here's some LESS code: + // + // .class { + // color: #fff; + // border: 1px solid #000; + // width: @w + 4px; + // > .child {...} + // } + // + // And here's what the parse tree might look like: + // + // Ruleset (Selector '.class', [ + // Rule ("color", Value ([Expression [Color #fff]])) + // Rule ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]])) + // Rule ("width", Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]])) + // Ruleset (Selector [Element '>', '.child'], [...]) + // ]) + // + // In general, most rules will try to parse a token with the `$()` function, and if the return + // value is truly, will return a new node, of the relevant type. Sometimes, we need to check + // first, before parsing, that's when we use `peek()`. + // + parsers: { + // + // The `primary` rule is the *entry* and *exit* point of the parser. + // The rules here can appear at any level of the parse tree. + // + // The recursive nature of the grammar is an interplay between the `block` + // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule, + // as represented by this simplified grammar: + // + // primary → (ruleset | rule)+ + // ruleset → selector+ block + // block → '{' primary '}' + // + // Only at one point is the primary rule not called from the + // block rule: at the root level. + // + primary: function () { + var node, root = []; + + while ((node = $(this.mixin.definition) || $(this.rule) || $(this.ruleset) || + $(this.mixin.call) || $(this.comment) || $(this.directive)) + || $(/^[\s\n]+/)) { + node && root.push(node); + } + return root; + }, + + // We create a Comment node for CSS comments `/* */`, + // but keep the LeSS comments `//` silent, by just skipping + // over them. + comment: function () { + var comment; + + if (input.charAt(i) !== '/') return; + + if (input.charAt(i + 1) === '/') { + return new(tree.Comment)($(/^\/\/.*/), true); + } else if (comment = $(/^\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/)) { + return new(tree.Comment)(comment); + } + }, + + // + // Entities are tokens which can be found inside an Expression + // + entities: { + // + // A string, which supports escaping " and ' + // + // "milky way" 'he\'s the one!' + // + quoted: function () { + var str, j = i, e; + + if (input.charAt(j) === '~') { j++, e = true } // Escaped strings + if (input.charAt(j) !== '"' && input.charAt(j) !== "'") return; + + e && $('~'); + + if (str = $(/^"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'/)) { + return new(tree.Quoted)(str[0], str[1] || str[2], e); + } + }, + + // + // A catch-all word, such as: + // + // black border-collapse + // + keyword: function () { + var k; + + if (k = $(/^[_A-Za-z-][_A-Za-z0-9-]*/)) { + if (tree.colors.hasOwnProperty(k)) { + // detect named color + return new(tree.Color)(tree.colors[k].slice(1)); + } else { + return new(tree.Keyword)(k); + } + } + }, + + // + // A function call + // + // rgb(255, 0, 255) + // + // We also try to catch IE's `alpha()`, but let the `alpha` parser + // deal with the details. + // + // The arguments are parsed with the `entities.arguments` parser. + // + call: function () { + var name, args, index = i; + + if (! (name = /^([\w-]+|%|progid:[\w\.]+)\(/.exec(chunks[j]))) return; + + name = name[1].toLowerCase(); + + if (name === 'url') { return null } + else { i += name.length } + + if (name === 'alpha') { return $(this.alpha) } + + $('('); // Parse the '(' and consume whitespace. + + args = $(this.entities.arguments); + + if (! $(')')) return; + + if (name) { return new(tree.Call)(name, args, index, env.filename) } + }, + arguments: function () { + var args = [], arg; + + while (arg = $(this.entities.assignment) || $(this.expression)) { + args.push(arg); + if (! $(',')) { break } + } + return args; + }, + literal: function () { + return $(this.entities.dimension) || + $(this.entities.color) || + $(this.entities.quoted); + }, + + // Assignments are argument entities for calls. + // They are present in ie filter properties as shown below. + // + // filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* ) + // + + assignment: function () { + var key, value; + if ((key = $(/^\w+(?=\s?=)/i)) && $('=') && (value = $(this.entity))) { + return new(tree.Assignment)(key, value); + } + }, + + // + // Parse url() tokens + // + // We use a specific rule for urls, because they don't really behave like + // standard function calls. The difference is that the argument doesn't have + // to be enclosed within a string, so it can't be parsed as an Expression. + // + url: function () { + var value; + + if (input.charAt(i) !== 'u' || !$(/^url\(/)) return; + value = $(this.entities.quoted) || $(this.entities.variable) || + $(this.entities.dataURI) || $(/^[-\w%@$\/.&=:;#+?~]+/) || ""; + + expect(')'); + + return new(tree.URL)((value.value || value.data || value instanceof tree.Variable) + ? value : new(tree.Anonymous)(value), imports.paths); + }, + + dataURI: function () { + var obj; + + if ($(/^data:/)) { + obj = {}; + obj.mime = $(/^[^\/]+\/[^,;)]+/) || ''; + obj.charset = $(/^;\s*charset=[^,;)]+/) || ''; + obj.base64 = $(/^;\s*base64/) || ''; + obj.data = $(/^,\s*[^)]+/); + + if (obj.data) { return obj } + } + }, + + // + // A Variable entity, such as `@fink`, in + // + // width: @fink + 2px + // + // We use a different parser for variable definitions, + // see `parsers.variable`. + // + variable: function () { + var name, index = i; + + if (input.charAt(i) === '@' && (name = $(/^@@?[\w-]+/))) { + return new(tree.Variable)(name, index, env.filename); + } + }, + + // + // A Hexadecimal color + // + // #4F3C2F + // + // `rgb` and `hsl` colors are parsed through the `entities.call` parser. + // + color: function () { + var rgb; + + if (input.charAt(i) === '#' && (rgb = $(/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})/))) { + return new(tree.Color)(rgb[1]); + } + }, + + // + // A Dimension, that is, a number and a unit + // + // 0.5em 95% + // + dimension: function () { + var value, c = input.charCodeAt(i); + if ((c > 57 || c < 45) || c === 47) return; + + if (value = $(/^(-?\d*\.?\d+)(px|%|em|rem|pc|ex|in|deg|s|ms|pt|cm|mm|rad|grad|turn|dpi)?/)) { + return new(tree.Dimension)(value[1], value[2]); + } + }, + + // + // JavaScript code to be evaluated + // + // `window.location.href` + // + javascript: function () { + var str, j = i, e; + + if (input.charAt(j) === '~') { j++, e = true } // Escaped strings + if (input.charAt(j) !== '`') { return } + + e && $('~'); + + if (str = $(/^`([^`]*)`/)) { + return new(tree.JavaScript)(str[1], i, e); + } + } + }, + + // + // The variable part of a variable definition. Used in the `rule` parser + // + // @fink: + // + variable: function () { + var name; + + if (input.charAt(i) === '@' && (name = $(/^(@[\w-]+)\s*:/))) { return name[1] } + }, + + // + // A font size/line-height shorthand + // + // small/12px + // + // We need to peek first, or we'll match on keywords and dimensions + // + shorthand: function () { + var a, b; + + if (! peek(/^[@\w.%-]+\/[@\w.-]+/)) return; + + if ((a = $(this.entity)) && $('/') && (b = $(this.entity))) { + return new(tree.Shorthand)(a, b); + } + }, + + // + // Mixins + // + mixin: { + // + // A Mixin call, with an optional argument list + // + // #mixins > .square(#fff); + // .rounded(4px, black); + // .button; + // + // The `while` loop is there because mixins can be + // namespaced, but we only support the child and descendant + // selector for now. + // + call: function () { + var elements = [], e, c, args = [], arg, index = i, s = input.charAt(i), name, value, important = false; + + if (s !== '.' && s !== '#') { return } + + while (e = $(/^[#.](?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+/)) { + elements.push(new(tree.Element)(c, e, i)); + c = $('>'); + } + if ($('(')) { + while (arg = $(this.expression)) { + value = arg; + name = null; + + // Variable + if (arg.value.length == 1) { + var val = arg.value[0]; + if (val instanceof tree.Variable) { + if ($(':')) { + if (value = $(this.expression)) { + name = val.name; + } else { + throw new(Error)("Expected value"); + } + } + } + } + + args.push({ name: name, value: value }); + + if (! $(',')) { break } + } + if (! $(')')) throw new(Error)("Expected )"); + } + + if ($(this.important)) { + important = true; + } + + if (elements.length > 0 && ($(';') || peek('}'))) { + return new(tree.mixin.Call)(elements, args, index, env.filename, important); + } + }, + + // + // A Mixin definition, with a list of parameters + // + // .rounded (@radius: 2px, @color) { + // ... + // } + // + // Until we have a finer grained state-machine, we have to + // do a look-ahead, to make sure we don't have a mixin call. + // See the `rule` function for more information. + // + // We start by matching `.rounded (`, and then proceed on to + // the argument list, which has optional default values. + // We store the parameters in `params`, with a `value` key, + // if there is a value, such as in the case of `@radius`. + // + // Once we've got our params list, and a closing `)`, we parse + // the `{...}` block. + // + definition: function () { + var name, params = [], match, ruleset, param, value, cond, variadic = false; + if ((input.charAt(i) !== '.' && input.charAt(i) !== '#') || + peek(/^[^{]*(;|})/)) return; + + save(); + + if (match = $(/^([#.](?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+)\s*\(/)) { + name = match[1]; + + do { + if (input.charAt(i) === '.' && $(/^\.{3}/)) { + variadic = true; + break; + } else if (param = $(this.entities.variable) || $(this.entities.literal) + || $(this.entities.keyword)) { + // Variable + if (param instanceof tree.Variable) { + if ($(':')) { + value = expect(this.expression, 'expected expression'); + params.push({ name: param.name, value: value }); + } else if ($(/^\.{3}/)) { + params.push({ name: param.name, variadic: true }); + variadic = true; + break; + } else { + params.push({ name: param.name }); + } + } else { + params.push({ value: param }); + } + } else { + break; + } + } while ($(',')) + + expect(')'); + + if ($(/^when/)) { // Guard + cond = expect(this.conditions, 'expected condition'); + } + + ruleset = $(this.block); + + if (ruleset) { + return new(tree.mixin.Definition)(name, params, ruleset, cond, variadic); + } else { + restore(); + } + } + } + }, + + // + // Entities are the smallest recognized token, + // and can be found inside a rule's value. + // + entity: function () { + return $(this.entities.literal) || $(this.entities.variable) || $(this.entities.url) || + $(this.entities.call) || $(this.entities.keyword) || $(this.entities.javascript) || + $(this.comment); + }, + + // + // A Rule terminator. Note that we use `peek()` to check for '}', + // because the `block` rule will be expecting it, but we still need to make sure + // it's there, if ';' was ommitted. + // + end: function () { + return $(';') || peek('}'); + }, + + // + // IE's alpha function + // + // alpha(opacity=88) + // + alpha: function () { + var value; + + if (! $(/^\(opacity=/i)) return; + if (value = $(/^\d+/) || $(this.entities.variable)) { + expect(')'); + return new(tree.Alpha)(value); + } + }, + + // + // A Selector Element + // + // div + // + h1 + // #socks + // input[type="text"] + // + // Elements are the building blocks for Selectors, + // they are made out of a `Combinator` (see combinator rule), + // and an element name, such as a tag a class, or `*`. + // + element: function () { + var e, t, c, v; + + c = $(this.combinator); + e = $(/^(?:\d+\.\d+|\d+)%/) || $(/^(?:[.#]?|:*)(?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+/) || + $('*') || $(this.attribute) || $(/^\([^)@]+\)/); + + if (! e) { + $('(') && (v = $(this.entities.variable)) && $(')') && (e = new(tree.Paren)(v)); + } + + if (e) { return new(tree.Element)(c, e, i) } + + if (c.value && c.value.charAt(0) === '&') { + return new(tree.Element)(c, null, i); + } + }, + + // + // Combinators combine elements together, in a Selector. + // + // Because our parser isn't white-space sensitive, special care + // has to be taken, when parsing the descendant combinator, ` `, + // as it's an empty space. We have to check the previous character + // in the input, to see if it's a ` ` character. More info on how + // we deal with this in *combinator.js*. + // + combinator: function () { + var match, c = input.charAt(i); + + if (c === '>' || c === '+' || c === '~') { + i++; + while (input.charAt(i) === ' ') { i++ } + return new(tree.Combinator)(c); + } else if (c === '&') { + match = '&'; + i++; + if(input.charAt(i) === ' ') { + match = '& '; + } + while (input.charAt(i) === ' ') { i++ } + return new(tree.Combinator)(match); + } else if (input.charAt(i - 1) === ' ') { + return new(tree.Combinator)(" "); + } else { + return new(tree.Combinator)(null); + } + }, + + // + // A CSS Selector + // + // .class > div + h1 + // li a:hover + // + // Selectors are made out of one or more Elements, see above. + // + selector: function () { + var sel, e, elements = [], c, match; + + if ($('(')) { + sel = $(this.entity); + expect(')'); + return new(tree.Selector)([new(tree.Element)('', sel, i)]); + } + + while (e = $(this.element)) { + c = input.charAt(i); + elements.push(e) + if (c === '{' || c === '}' || c === ';' || c === ',') { break } + } + + if (elements.length > 0) { return new(tree.Selector)(elements) } + }, + tag: function () { + return $(/^[a-zA-Z][a-zA-Z-]*[0-9]?/) || $('*'); + }, + attribute: function () { + var attr = '', key, val, op; + + if (! $('[')) return; + + if (key = $(/^[a-zA-Z-]+/) || $(this.entities.quoted)) { + if ((op = $(/^[|~*$^]?=/)) && + (val = $(this.entities.quoted) || $(/^[\w-]+/))) { + attr = [key, op, val.toCSS ? val.toCSS() : val].join(''); + } else { attr = key } + } + + if (! $(']')) return; + + if (attr) { return "[" + attr + "]" } + }, + + // + // The `block` rule is used by `ruleset` and `mixin.definition`. + // It's a wrapper around the `primary` rule, with added `{}`. + // + block: function () { + var content; + + if ($('{') && (content = $(this.primary)) && $('}')) { + return content; + } + }, + + // + // div, .class, body > p {...} + // + ruleset: function () { + var selectors = [], s, rules, match; + save(); + + while (s = $(this.selector)) { + selectors.push(s); + $(this.comment); + if (! $(',')) { break } + $(this.comment); + } + + if (selectors.length > 0 && (rules = $(this.block))) { + return new(tree.Ruleset)(selectors, rules, env.strictImports); + } else { + // Backtrack + furthest = i; + restore(); + } + }, + rule: function () { + var name, value, c = input.charAt(i), important, match; + save(); + + if (c === '.' || c === '#' || c === '&') { return } + + if (name = $(this.variable) || $(this.property)) { + if ((name.charAt(0) != '@') && (match = /^([^@+\/'"*`(;{}-]*);/.exec(chunks[j]))) { + i += match[0].length - 1; + value = new(tree.Anonymous)(match[1]); + } else if (name === "font") { + value = $(this.font); + } else { + value = $(this.value); + } + important = $(this.important); + + if (value && $(this.end)) { + return new(tree.Rule)(name, value, important, memo); + } else { + furthest = i; + restore(); + } + } + }, + + // + // An @import directive + // + // @import "lib"; + // + // Depending on our environemnt, importing is done differently: + // In the browser, it's an XHR request, in Node, it would be a + // file-system operation. The function used for importing is + // stored in `import`, which we pass to the Import constructor. + // + "import": function () { + var path, features, index = i; + var dir = $(/^@import(?:-(once))?\s+/); + + if (dir && (path = $(this.entities.quoted) || $(this.entities.url))) { + features = $(this.mediaFeatures); + if ($(';')) { + return new(tree.Import)(path, imports, features, (dir[1] === 'once'), index); + } + } + }, + + mediaFeature: function () { + var e, p, nodes = []; + + do { + if (e = $(this.entities.keyword)) { + nodes.push(e); + } else if ($('(')) { + p = $(this.property); + e = $(this.entity); + if ($(')')) { + if (p && e) { + nodes.push(new(tree.Paren)(new(tree.Rule)(p, e, null, i, true))); + } else if (e) { + nodes.push(new(tree.Paren)(e)); + } else { + return null; + } + } else { return null } + } + } while (e); + + if (nodes.length > 0) { + return new(tree.Expression)(nodes); + } + }, + + mediaFeatures: function () { + var e, features = []; + + do { + if (e = $(this.mediaFeature)) { + features.push(e); + if (! $(',')) { break } + } else if (e = $(this.entities.variable)) { + features.push(e); + if (! $(',')) { break } + } + } while (e); + + return features.length > 0 ? features : null; + }, + + media: function () { + var features, rules; + + if ($(/^@media/)) { + features = $(this.mediaFeatures); + + if (rules = $(this.block)) { + return new(tree.Media)(rules, features); + } + } + }, + + // + // A CSS Directive + // + // @charset "utf-8"; + // + directive: function () { + var name, value, rules, types, e, nodes; + + if (input.charAt(i) !== '@') return; + + if (value = $(this['import']) || $(this.media)) { + return value; + } else if (name = $(/^@page|@keyframes/) || $(/^@(?:-webkit-|-moz-|-o-|-ms-)[a-z0-9-]+/)) { + types = ($(/^[^{]+/) || '').trim(); + if (rules = $(this.block)) { + return new(tree.Directive)(name + " " + types, rules); + } + } else if (name = $(/^@[-a-z]+/)) { + if (name === '@font-face') { + if (rules = $(this.block)) { + return new(tree.Directive)(name, rules); + } + } else if ((value = $(this.entity)) && $(';')) { + return new(tree.Directive)(name, value); + } + } + }, + font: function () { + var value = [], expression = [], weight, shorthand, font, e; + + while (e = $(this.shorthand) || $(this.entity)) { + expression.push(e); + } + value.push(new(tree.Expression)(expression)); + + if ($(',')) { + while (e = $(this.expression)) { + value.push(e); + if (! $(',')) { break } + } + } + return new(tree.Value)(value); + }, + + // + // A Value is a comma-delimited list of Expressions + // + // font-family: Baskerville, Georgia, serif; + // + // In a Rule, a Value represents everything after the `:`, + // and before the `;`. + // + value: function () { + var e, expressions = [], important; + + while (e = $(this.expression)) { + expressions.push(e); + if (! $(',')) { break } + } + + if (expressions.length > 0) { + return new(tree.Value)(expressions); + } + }, + important: function () { + if (input.charAt(i) === '!') { + return $(/^! *important/); + } + }, + sub: function () { + var e; + + if ($('(') && (e = $(this.expression)) && $(')')) { + return e; + } + }, + multiplication: function () { + var m, a, op, operation; + if (m = $(this.operand)) { + while (!peek(/^\/\*/) && (op = ($('/') || $('*'))) && (a = $(this.operand))) { + operation = new(tree.Operation)(op, [operation || m, a]); + } + return operation || m; + } + }, + addition: function () { + var m, a, op, operation; + if (m = $(this.multiplication)) { + while ((op = $(/^[-+]\s+/) || (input.charAt(i - 1) != ' ' && ($('+') || $('-')))) && + (a = $(this.multiplication))) { + operation = new(tree.Operation)(op, [operation || m, a]); + } + return operation || m; + } + }, + conditions: function () { + var a, b, index = i, condition; + + if (a = $(this.condition)) { + while ($(',') && (b = $(this.condition))) { + condition = new(tree.Condition)('or', condition || a, b, index); + } + return condition || a; + } + }, + condition: function () { + var a, b, c, op, index = i, negate = false; + + if ($(/^not/)) { negate = true } + expect('('); + if (a = $(this.addition) || $(this.entities.keyword) || $(this.entities.quoted)) { + if (op = $(/^(?:>=|=<|[<=>])/)) { + if (b = $(this.addition) || $(this.entities.keyword) || $(this.entities.quoted)) { + c = new(tree.Condition)(op, a, b, index, negate); + } else { + error('expected expression'); + } + } else { + c = new(tree.Condition)('=', a, new(tree.Keyword)('true'), index, negate); + } + expect(')'); + return $(/^and/) ? new(tree.Condition)('and', c, $(this.condition)) : c; + } + }, + + // + // An operand is anything that can be part of an operation, + // such as a Color, or a Variable + // + operand: function () { + var negate, p = input.charAt(i + 1); + + if (input.charAt(i) === '-' && (p === '@' || p === '(')) { negate = $('-') } + var o = $(this.sub) || $(this.entities.dimension) || + $(this.entities.color) || $(this.entities.variable) || + $(this.entities.call); + return negate ? new(tree.Operation)('*', [new(tree.Dimension)(-1), o]) + : o; + }, + + // + // Expressions either represent mathematical operations, + // or white-space delimited Entities. + // + // 1px solid black + // @var * 2 + // + expression: function () { + var e, delim, entities = [], d; + + while (e = $(this.addition) || $(this.entity)) { + entities.push(e); + } + if (entities.length > 0) { + return new(tree.Expression)(entities); + } + }, + property: function () { + var name; + + if (name = $(/^(\*?-?[-a-z_0-9]+)\s*:/)) { + return name[1]; + } + } + } + }; +}; + +if (less.mode === 'browser' || less.mode === 'rhino') { + // + // Used by `@import` directives + // + less.Parser.importer = function (path, paths, callback, env) { + if (!/^([a-z]+:)?\//.test(path) && paths.length > 0) { + path = paths[0] + path; + } + // We pass `true` as 3rd argument, to force the reload of the import. + // This is so we can get the syntax tree as opposed to just the CSS output, + // as we need this to evaluate the current stylesheet. + loadStyleSheet({ href: path, title: path, type: env.mime }, function (e) { + if (e && typeof(env.errback) === "function") { + env.errback.call(null, path, paths, callback, env); + } else { + callback.apply(null, arguments); + } + }, true); + }; +} + diff --git a/dashboard/bin/lib/less/rhino.js b/dashboard/bin/lib/less/rhino.js new file mode 100644 index 00000000..a2c5662f --- /dev/null +++ b/dashboard/bin/lib/less/rhino.js @@ -0,0 +1,62 @@ +var name; + +function loadStyleSheet(sheet, callback, reload, remaining) { + var sheetName = name.slice(0, name.lastIndexOf('/') + 1) + sheet.href; + var input = readFile(sheetName); + var parser = new less.Parser({ + paths: [sheet.href.replace(/[\w\.-]+$/, '')] + }); + parser.parse(input, function (e, root) { + if (e) { + print("Error: " + e); + quit(1); + } + callback(root, sheet, { local: false, lastModified: 0, remaining: remaining }); + }); + + // callback({}, sheet, { local: true, remaining: remaining }); +} + +function writeFile(filename, content) { + var fstream = new java.io.FileWriter(filename); + var out = new java.io.BufferedWriter(fstream); + out.write(content); + out.close(); +} + +// Command line integration via Rhino +(function (args) { + name = args[0]; + var output = args[1]; + + if (!name) { + print('No files present in the fileset; Check your pattern match in build.xml'); + quit(1); + } + path = name.split("/");path.pop();path=path.join("/") + + var input = readFile(name); + + if (!input) { + print('lesscss: couldn\'t open file ' + name); + quit(1); + } + + var result; + var parser = new less.Parser(); + parser.parse(input, function (e, root) { + if (e) { + quit(1); + } else { + result = root.toCSS(); + if (output) { + writeFile(output, result); + print("Written to " + output); + } else { + print(result); + } + quit(0); + } + }); + print("done"); +}(arguments)); diff --git a/dashboard/bin/lib/less/tree.js b/dashboard/bin/lib/less/tree.js new file mode 100644 index 00000000..24ecd712 --- /dev/null +++ b/dashboard/bin/lib/less/tree.js @@ -0,0 +1,17 @@ +(function (tree) { + +tree.find = function (obj, fun) { + for (var i = 0, r; i < obj.length; i++) { + if (r = fun.call(obj, obj[i])) { return r } + } + return null; +}; +tree.jsify = function (obj) { + if (Array.isArray(obj.value) && (obj.value.length > 1)) { + return '[' + obj.value.map(function (v) { return v.toCSS(false) }).join(', ') + ']'; + } else { + return obj.toCSS(false); + } +}; + +})(require('./tree')); diff --git a/dashboard/bin/lib/less/tree/alpha.js b/dashboard/bin/lib/less/tree/alpha.js new file mode 100644 index 00000000..139ae920 --- /dev/null +++ b/dashboard/bin/lib/less/tree/alpha.js @@ -0,0 +1,17 @@ +(function (tree) { + +tree.Alpha = function (val) { + this.value = val; +}; +tree.Alpha.prototype = { + toCSS: function () { + return "alpha(opacity=" + + (this.value.toCSS ? this.value.toCSS() : this.value) + ")"; + }, + eval: function (env) { + if (this.value.eval) { this.value = this.value.eval(env) } + return this; + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/anonymous.js b/dashboard/bin/lib/less/tree/anonymous.js new file mode 100644 index 00000000..460c9ec7 --- /dev/null +++ b/dashboard/bin/lib/less/tree/anonymous.js @@ -0,0 +1,13 @@ +(function (tree) { + +tree.Anonymous = function (string) { + this.value = string.value || string; +}; +tree.Anonymous.prototype = { + toCSS: function () { + return this.value; + }, + eval: function () { return this } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/assignment.js b/dashboard/bin/lib/less/tree/assignment.js new file mode 100644 index 00000000..70ce6e2f --- /dev/null +++ b/dashboard/bin/lib/less/tree/assignment.js @@ -0,0 +1,17 @@ +(function (tree) { + +tree.Assignment = function (key, val) { + this.key = key; + this.value = val; +}; +tree.Assignment.prototype = { + toCSS: function () { + return this.key + '=' + (this.value.toCSS ? this.value.toCSS() : this.value); + }, + eval: function (env) { + if (this.value.eval) { this.value = this.value.eval(env) } + return this; + } +}; + +})(require('../tree')); \ No newline at end of file diff --git a/dashboard/bin/lib/less/tree/call.js b/dashboard/bin/lib/less/tree/call.js new file mode 100644 index 00000000..c1465dd4 --- /dev/null +++ b/dashboard/bin/lib/less/tree/call.js @@ -0,0 +1,48 @@ +(function (tree) { + +// +// A function call node. +// +tree.Call = function (name, args, index, filename) { + this.name = name; + this.args = args; + this.index = index; + this.filename = filename; +}; +tree.Call.prototype = { + // + // When evaluating a function call, + // we either find the function in `tree.functions` [1], + // in which case we call it, passing the evaluated arguments, + // or we simply print it out as it appeared originally [2]. + // + // The *functions.js* file contains the built-in functions. + // + // The reason why we evaluate the arguments, is in the case where + // we try to pass a variable to a function, like: `saturate(@color)`. + // The function should receive the value, not the variable. + // + eval: function (env) { + var args = this.args.map(function (a) { return a.eval(env) }); + + if (this.name in tree.functions) { // 1. + try { + return tree.functions[this.name].apply(tree.functions, args); + } catch (e) { + throw { type: e.type || "Runtime", + message: "error evaluating function `" + this.name + "`" + + (e.message ? ': ' + e.message : ''), + index: this.index, filename: this.filename }; + } + } else { // 2. + return new(tree.Anonymous)(this.name + + "(" + args.map(function (a) { return a.toCSS() }).join(', ') + ")"); + } + }, + + toCSS: function (env) { + return this.eval(env).toCSS(); + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/color.js b/dashboard/bin/lib/less/tree/color.js new file mode 100644 index 00000000..37ce1781 --- /dev/null +++ b/dashboard/bin/lib/less/tree/color.js @@ -0,0 +1,101 @@ +(function (tree) { +// +// RGB Colors - #ff0014, #eee +// +tree.Color = function (rgb, a) { + // + // The end goal here, is to parse the arguments + // into an integer triplet, such as `128, 255, 0` + // + // This facilitates operations and conversions. + // + if (Array.isArray(rgb)) { + this.rgb = rgb; + } else if (rgb.length == 6) { + this.rgb = rgb.match(/.{2}/g).map(function (c) { + return parseInt(c, 16); + }); + } else { + this.rgb = rgb.split('').map(function (c) { + return parseInt(c + c, 16); + }); + } + this.alpha = typeof(a) === 'number' ? a : 1; +}; +tree.Color.prototype = { + eval: function () { return this }, + + // + // If we have some transparency, the only way to represent it + // is via `rgba`. Otherwise, we use the hex representation, + // which has better compatibility with older browsers. + // Values are capped between `0` and `255`, rounded and zero-padded. + // + toCSS: function () { + if (this.alpha < 1.0) { + return "rgba(" + this.rgb.map(function (c) { + return Math.round(c); + }).concat(this.alpha).join(', ') + ")"; + } else { + return '#' + this.rgb.map(function (i) { + i = Math.round(i); + i = (i > 255 ? 255 : (i < 0 ? 0 : i)).toString(16); + return i.length === 1 ? '0' + i : i; + }).join(''); + } + }, + + // + // Operations have to be done per-channel, if not, + // channels will spill onto each other. Once we have + // our result, in the form of an integer triplet, + // we create a new Color node to hold the result. + // + operate: function (op, other) { + var result = []; + + if (! (other instanceof tree.Color)) { + other = other.toColor(); + } + + for (var c = 0; c < 3; c++) { + result[c] = tree.operate(op, this.rgb[c], other.rgb[c]); + } + return new(tree.Color)(result, this.alpha + other.alpha); + }, + + toHSL: function () { + var r = this.rgb[0] / 255, + g = this.rgb[1] / 255, + b = this.rgb[2] / 255, + a = this.alpha; + + var max = Math.max(r, g, b), min = Math.min(r, g, b); + var h, s, l = (max + min) / 2, d = max - min; + + if (max === min) { + h = s = 0; + } else { + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + return { h: h * 360, s: s, l: l, a: a }; + }, + toARGB: function () { + var argb = [Math.round(this.alpha * 255)].concat(this.rgb); + return '#' + argb.map(function (i) { + i = Math.round(i); + i = (i > 255 ? 255 : (i < 0 ? 0 : i)).toString(16); + return i.length === 1 ? '0' + i : i; + }).join(''); + } +}; + + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/comment.js b/dashboard/bin/lib/less/tree/comment.js new file mode 100644 index 00000000..f4a33840 --- /dev/null +++ b/dashboard/bin/lib/less/tree/comment.js @@ -0,0 +1,14 @@ +(function (tree) { + +tree.Comment = function (value, silent) { + this.value = value; + this.silent = !!silent; +}; +tree.Comment.prototype = { + toCSS: function (env) { + return env.compress ? '' : this.value; + }, + eval: function () { return this } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/condition.js b/dashboard/bin/lib/less/tree/condition.js new file mode 100644 index 00000000..6b79dc96 --- /dev/null +++ b/dashboard/bin/lib/less/tree/condition.js @@ -0,0 +1,42 @@ +(function (tree) { + +tree.Condition = function (op, l, r, i, negate) { + this.op = op.trim(); + this.lvalue = l; + this.rvalue = r; + this.index = i; + this.negate = negate; +}; +tree.Condition.prototype.eval = function (env) { + var a = this.lvalue.eval(env), + b = this.rvalue.eval(env); + + var i = this.index, result; + + var result = (function (op) { + switch (op) { + case 'and': + return a && b; + case 'or': + return a || b; + default: + if (a.compare) { + result = a.compare(b); + } else if (b.compare) { + result = b.compare(a); + } else { + throw { type: "Type", + message: "Unable to perform comparison", + index: i }; + } + switch (result) { + case -1: return op === '<' || op === '=<'; + case 0: return op === '=' || op === '>=' || op === '=<'; + case 1: return op === '>' || op === '>='; + } + } + })(this.op); + return this.negate ? !result : result; +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/dimension.js b/dashboard/bin/lib/less/tree/dimension.js new file mode 100644 index 00000000..9a6fce3d --- /dev/null +++ b/dashboard/bin/lib/less/tree/dimension.js @@ -0,0 +1,49 @@ +(function (tree) { + +// +// A number with a unit +// +tree.Dimension = function (value, unit) { + this.value = parseFloat(value); + this.unit = unit || null; +}; + +tree.Dimension.prototype = { + eval: function () { return this }, + toColor: function () { + return new(tree.Color)([this.value, this.value, this.value]); + }, + toCSS: function () { + var css = this.value + this.unit; + return css; + }, + + // In an operation between two Dimensions, + // we default to the first Dimension's unit, + // so `1px + 2em` will yield `3px`. + // In the future, we could implement some unit + // conversions such that `100cm + 10mm` would yield + // `101cm`. + operate: function (op, other) { + return new(tree.Dimension) + (tree.operate(op, this.value, other.value), + this.unit || other.unit); + }, + + // TODO: Perform unit conversion before comparing + compare: function (other) { + if (other instanceof tree.Dimension) { + if (other.value > this.value) { + return -1; + } else if (other.value < this.value) { + return 1; + } else { + return 0; + } + } else { + return -1; + } + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/directive.js b/dashboard/bin/lib/less/tree/directive.js new file mode 100644 index 00000000..27538332 --- /dev/null +++ b/dashboard/bin/lib/less/tree/directive.js @@ -0,0 +1,35 @@ +(function (tree) { + +tree.Directive = function (name, value, features) { + this.name = name; + + if (Array.isArray(value)) { + this.ruleset = new(tree.Ruleset)([], value); + this.ruleset.allowImports = true; + } else { + this.value = value; + } +}; +tree.Directive.prototype = { + toCSS: function (ctx, env) { + if (this.ruleset) { + this.ruleset.root = true; + return this.name + (env.compress ? '{' : ' {\n ') + + this.ruleset.toCSS(ctx, env).trim().replace(/\n/g, '\n ') + + (env.compress ? '}': '\n}\n'); + } else { + return this.name + ' ' + this.value.toCSS() + ';\n'; + } + }, + eval: function (env) { + env.frames.unshift(this); + this.ruleset = this.ruleset && this.ruleset.eval(env); + env.frames.shift(); + return this; + }, + variable: function (name) { return tree.Ruleset.prototype.variable.call(this.ruleset, name) }, + find: function () { return tree.Ruleset.prototype.find.apply(this.ruleset, arguments) }, + rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.ruleset) } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/element.js b/dashboard/bin/lib/less/tree/element.js new file mode 100644 index 00000000..14b08d2e --- /dev/null +++ b/dashboard/bin/lib/less/tree/element.js @@ -0,0 +1,52 @@ +(function (tree) { + +tree.Element = function (combinator, value, index) { + this.combinator = combinator instanceof tree.Combinator ? + combinator : new(tree.Combinator)(combinator); + + if (typeof(value) === 'string') { + this.value = value.trim(); + } else if (value) { + this.value = value; + } else { + this.value = ""; + } + this.index = index; +}; +tree.Element.prototype.eval = function (env) { + return new(tree.Element)(this.combinator, + this.value.eval ? this.value.eval(env) : this.value, + this.index); +}; +tree.Element.prototype.toCSS = function (env) { + var value = (this.value.toCSS ? this.value.toCSS(env) : this.value); + if (value == '' && this.combinator.value.charAt(0) == '&') { + return ''; + } else { + return this.combinator.toCSS(env || {}) + value; + } +}; + +tree.Combinator = function (value) { + if (value === ' ') { + this.value = ' '; + } else if (value === '& ') { + this.value = '& '; + } else { + this.value = value ? value.trim() : ""; + } +}; +tree.Combinator.prototype.toCSS = function (env) { + return { + '' : '', + ' ' : ' ', + '&' : '', + '& ' : ' ', + ':' : ' :', + '+' : env.compress ? '+' : ' + ', + '~' : env.compress ? '~' : ' ~ ', + '>' : env.compress ? '>' : ' > ' + }[this.value]; +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/expression.js b/dashboard/bin/lib/less/tree/expression.js new file mode 100644 index 00000000..fbfa9c5b --- /dev/null +++ b/dashboard/bin/lib/less/tree/expression.js @@ -0,0 +1,23 @@ +(function (tree) { + +tree.Expression = function (value) { this.value = value }; +tree.Expression.prototype = { + eval: function (env) { + if (this.value.length > 1) { + return new(tree.Expression)(this.value.map(function (e) { + return e.eval(env); + })); + } else if (this.value.length === 1) { + return this.value[0].eval(env); + } else { + return this; + } + }, + toCSS: function (env) { + return this.value.map(function (e) { + return e.toCSS ? e.toCSS(env) : ''; + }).join(' '); + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/import.js b/dashboard/bin/lib/less/tree/import.js new file mode 100644 index 00000000..7a977def --- /dev/null +++ b/dashboard/bin/lib/less/tree/import.js @@ -0,0 +1,83 @@ +(function (tree) { +// +// CSS @import node +// +// The general strategy here is that we don't want to wait +// for the parsing to be completed, before we start importing +// the file. That's because in the context of a browser, +// most of the time will be spent waiting for the server to respond. +// +// On creation, we push the import path to our import queue, though +// `import,push`, we also pass it a callback, which it'll call once +// the file has been fetched, and parsed. +// +tree.Import = function (path, imports, features, once, index) { + var that = this; + + this.once = once; + this.index = index; + this._path = path; + this.features = features && new(tree.Value)(features); + + // The '.less' extension is optional + if (path instanceof tree.Quoted) { + this.path = /\.(le?|c)ss(\?.*)?$/.test(path.value) ? path.value : path.value + '.less'; + } else { + this.path = path.value.value || path.value; + } + + this.css = /css(\?.*)?$/.test(this.path); + + // Only pre-compile .less files + if (! this.css) { + imports.push(this.path, function (e, root, imported) { + if (e) { e.index = index } + if (imported && that.once) that.skip = imported; + that.root = root || new(tree.Ruleset)([], []); + }); + } +}; + +// +// The actual import node doesn't return anything, when converted to CSS. +// The reason is that it's used at the evaluation stage, so that the rules +// it imports can be treated like any other rules. +// +// In `eval`, we make sure all Import nodes get evaluated, recursively, so +// we end up with a flat structure, which can easily be imported in the parent +// ruleset. +// +tree.Import.prototype = { + toCSS: function (env) { + var features = this.features ? ' ' + this.features.toCSS(env) : ''; + + if (this.css) { + return "@import " + this._path.toCSS() + features + ';\n'; + } else { + return ""; + } + }, + eval: function (env) { + var ruleset, features = this.features && this.features.eval(env); + + if (this.skip) return []; + + if (this.css) { + return this; + } else { + ruleset = new(tree.Ruleset)([], this.root.rules.slice(0)); + + for (var i = 0; i < ruleset.rules.length; i++) { + if (ruleset.rules[i] instanceof tree.Import) { + Array.prototype + .splice + .apply(ruleset.rules, + [i, 1].concat(ruleset.rules[i].eval(env))); + } + } + return this.features ? new(tree.Media)(ruleset.rules, this.features.value) : ruleset.rules; + } + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/javascript.js b/dashboard/bin/lib/less/tree/javascript.js new file mode 100644 index 00000000..772a31dd --- /dev/null +++ b/dashboard/bin/lib/less/tree/javascript.js @@ -0,0 +1,51 @@ +(function (tree) { + +tree.JavaScript = function (string, index, escaped) { + this.escaped = escaped; + this.expression = string; + this.index = index; +}; +tree.JavaScript.prototype = { + eval: function (env) { + var result, + that = this, + context = {}; + + var expression = this.expression.replace(/@\{([\w-]+)\}/g, function (_, name) { + return tree.jsify(new(tree.Variable)('@' + name, that.index).eval(env)); + }); + + try { + expression = new(Function)('return (' + expression + ')'); + } catch (e) { + throw { message: "JavaScript evaluation error: `" + expression + "`" , + index: this.index }; + } + + for (var k in env.frames[0].variables()) { + context[k.slice(1)] = { + value: env.frames[0].variables()[k].value, + toJS: function () { + return this.value.eval(env).toCSS(); + } + }; + } + + try { + result = expression.call(context); + } catch (e) { + throw { message: "JavaScript evaluation error: '" + e.name + ': ' + e.message + "'" , + index: this.index }; + } + if (typeof(result) === 'string') { + return new(tree.Quoted)('"' + result + '"', result, this.escaped, this.index); + } else if (Array.isArray(result)) { + return new(tree.Anonymous)(result.join(', ')); + } else { + return new(tree.Anonymous)(result); + } + } +}; + +})(require('../tree')); + diff --git a/dashboard/bin/lib/less/tree/keyword.js b/dashboard/bin/lib/less/tree/keyword.js new file mode 100644 index 00000000..701b79e5 --- /dev/null +++ b/dashboard/bin/lib/less/tree/keyword.js @@ -0,0 +1,19 @@ +(function (tree) { + +tree.Keyword = function (value) { this.value = value }; +tree.Keyword.prototype = { + eval: function () { return this }, + toCSS: function () { return this.value }, + compare: function (other) { + if (other instanceof tree.Keyword) { + return other.value === this.value ? 0 : 1; + } else { + return -1; + } + } +}; + +tree.True = new(tree.Keyword)('true'); +tree.False = new(tree.Keyword)('false'); + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/media.js b/dashboard/bin/lib/less/tree/media.js new file mode 100644 index 00000000..2b7b26e5 --- /dev/null +++ b/dashboard/bin/lib/less/tree/media.js @@ -0,0 +1,114 @@ +(function (tree) { + +tree.Media = function (value, features) { + var el = new(tree.Element)('&', null, 0), + selectors = [new(tree.Selector)([el])]; + + this.features = new(tree.Value)(features); + this.ruleset = new(tree.Ruleset)(selectors, value); + this.ruleset.allowImports = true; +}; +tree.Media.prototype = { + toCSS: function (ctx, env) { + var features = this.features.toCSS(env); + + this.ruleset.root = (ctx.length === 0 || ctx[0].multiMedia); + return '@media ' + features + (env.compress ? '{' : ' {\n ') + + this.ruleset.toCSS(ctx, env).trim().replace(/\n/g, '\n ') + + (env.compress ? '}': '\n}\n'); + }, + eval: function (env) { + if (!env.mediaBlocks) { + env.mediaBlocks = []; + env.mediaPath = []; + } + + var blockIndex = env.mediaBlocks.length; + env.mediaPath.push(this); + env.mediaBlocks.push(this); + + var media = new(tree.Media)([], []); + media.features = this.features.eval(env); + + env.frames.unshift(this.ruleset); + media.ruleset = this.ruleset.eval(env); + env.frames.shift(); + + env.mediaBlocks[blockIndex] = media; + env.mediaPath.pop(); + + return env.mediaPath.length === 0 ? media.evalTop(env) : + media.evalNested(env) + }, + variable: function (name) { return tree.Ruleset.prototype.variable.call(this.ruleset, name) }, + find: function () { return tree.Ruleset.prototype.find.apply(this.ruleset, arguments) }, + rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.ruleset) }, + + evalTop: function (env) { + var result = this; + + // Render all dependent Media blocks. + if (env.mediaBlocks.length > 1) { + var el = new(tree.Element)('&', null, 0); + var selectors = [new(tree.Selector)([el])]; + result = new(tree.Ruleset)(selectors, env.mediaBlocks); + result.multiMedia = true; + } + + delete env.mediaBlocks; + delete env.mediaPath; + + return result; + }, + evalNested: function (env) { + var i, value, + path = env.mediaPath.concat([this]); + + // Extract the media-query conditions separated with `,` (OR). + for (i = 0; i < path.length; i++) { + value = path[i].features instanceof tree.Value ? + path[i].features.value : path[i].features; + path[i] = Array.isArray(value) ? value : [value]; + } + + // Trace all permutations to generate the resulting media-query. + // + // (a, b and c) with nested (d, e) -> + // a and d + // a and e + // b and c and d + // b and c and e + this.features = new(tree.Value)(this.permute(path).map(function (path) { + path = path.map(function (fragment) { + return fragment.toCSS ? fragment : new(tree.Anonymous)(fragment); + }); + + for(i = path.length - 1; i > 0; i--) { + path.splice(i, 0, new(tree.Anonymous)("and")); + } + + return new(tree.Expression)(path); + })); + + // Fake a tree-node that doesn't output anything. + return new(tree.Ruleset)([], []); + }, + permute: function (arr) { + if (arr.length === 0) { + return []; + } else if (arr.length === 1) { + return arr[0]; + } else { + var result = []; + var rest = this.permute(arr.slice(1)); + for (var i = 0; i < rest.length; i++) { + for (var j = 0; j < arr[0].length; j++) { + result.push([arr[0][j]].concat(rest[i])); + } + } + return result; + } + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/mixin.js b/dashboard/bin/lib/less/tree/mixin.js new file mode 100644 index 00000000..b441bf3b --- /dev/null +++ b/dashboard/bin/lib/less/tree/mixin.js @@ -0,0 +1,146 @@ +(function (tree) { + +tree.mixin = {}; +tree.mixin.Call = function (elements, args, index, filename, important) { + this.selector = new(tree.Selector)(elements); + this.arguments = args; + this.index = index; + this.filename = filename; + this.important = important; +}; +tree.mixin.Call.prototype = { + eval: function (env) { + var mixins, args, rules = [], match = false; + + for (var i = 0; i < env.frames.length; i++) { + if ((mixins = env.frames[i].find(this.selector)).length > 0) { + args = this.arguments && this.arguments.map(function (a) { + return { name: a.name, value: a.value.eval(env) }; + }); + for (var m = 0; m < mixins.length; m++) { + if (mixins[m].match(args, env)) { + try { + Array.prototype.push.apply( + rules, mixins[m].eval(env, this.arguments, this.important).rules); + match = true; + } catch (e) { + throw { message: e.message, index: this.index, filename: this.filename, stack: e.stack }; + } + } + } + if (match) { + return rules; + } else { + throw { type: 'Runtime', + message: 'No matching definition was found for `' + + this.selector.toCSS().trim() + '(' + + this.arguments.map(function (a) { + return a.toCSS(); + }).join(', ') + ")`", + index: this.index, filename: this.filename }; + } + } + } + throw { type: 'Name', + message: this.selector.toCSS().trim() + " is undefined", + index: this.index, filename: this.filename }; + } +}; + +tree.mixin.Definition = function (name, params, rules, condition, variadic) { + this.name = name; + this.selectors = [new(tree.Selector)([new(tree.Element)(null, name)])]; + this.params = params; + this.condition = condition; + this.variadic = variadic; + this.arity = params.length; + this.rules = rules; + this._lookups = {}; + this.required = params.reduce(function (count, p) { + if (!p.name || (p.name && !p.value)) { return count + 1 } + else { return count } + }, 0); + this.parent = tree.Ruleset.prototype; + this.frames = []; +}; +tree.mixin.Definition.prototype = { + toCSS: function () { return "" }, + variable: function (name) { return this.parent.variable.call(this, name) }, + variables: function () { return this.parent.variables.call(this) }, + find: function () { return this.parent.find.apply(this, arguments) }, + rulesets: function () { return this.parent.rulesets.apply(this) }, + + evalParams: function (env, args) { + var frame = new(tree.Ruleset)(null, []), varargs, arg; + + for (var i = 0, val, name; i < this.params.length; i++) { + arg = args && args[i] + + if (arg && arg.name) { + frame.rules.unshift(new(tree.Rule)(arg.name, arg.value.eval(env))); + args.splice(i, 1); + i--; + continue; + } + + if (name = this.params[i].name) { + if (this.params[i].variadic && args) { + varargs = []; + for (var j = i; j < args.length; j++) { + varargs.push(args[j].value.eval(env)); + } + frame.rules.unshift(new(tree.Rule)(name, new(tree.Expression)(varargs).eval(env))); + } else if (val = (arg && arg.value) || this.params[i].value) { + frame.rules.unshift(new(tree.Rule)(name, val.eval(env))); + } else { + throw { type: 'Runtime', message: "wrong number of arguments for " + this.name + + ' (' + args.length + ' for ' + this.arity + ')' }; + } + } + } + return frame; + }, + eval: function (env, args, important) { + var frame = this.evalParams(env, args), context, _arguments = [], rules, start; + + for (var i = 0; i < Math.max(this.params.length, args && args.length); i++) { + _arguments.push((args[i] && args[i].value) || this.params[i].value); + } + frame.rules.unshift(new(tree.Rule)('@arguments', new(tree.Expression)(_arguments).eval(env))); + + rules = important ? + this.rules.map(function (r) { + return new(tree.Rule)(r.name, r.value, '!important', r.index); + }) : this.rules.slice(0); + + return new(tree.Ruleset)(null, rules).eval({ + frames: [this, frame].concat(this.frames, env.frames) + }); + }, + match: function (args, env) { + var argsLength = (args && args.length) || 0, len, frame; + + if (! this.variadic) { + if (argsLength < this.required) { return false } + if (argsLength > this.params.length) { return false } + if ((this.required > 0) && (argsLength > this.params.length)) { return false } + } + + if (this.condition && !this.condition.eval({ + frames: [this.evalParams(env, args)].concat(env.frames) + })) { return false } + + len = Math.min(argsLength, this.arity); + + for (var i = 0; i < len; i++) { + if (!this.params[i].name) { + if (args[i].value.eval(env).toCSS() != this.params[i].value.eval(env).toCSS()) { + return false; + } + } + } + return true; + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/operation.js b/dashboard/bin/lib/less/tree/operation.js new file mode 100644 index 00000000..1ce22fb0 --- /dev/null +++ b/dashboard/bin/lib/less/tree/operation.js @@ -0,0 +1,32 @@ +(function (tree) { + +tree.Operation = function (op, operands) { + this.op = op.trim(); + this.operands = operands; +}; +tree.Operation.prototype.eval = function (env) { + var a = this.operands[0].eval(env), + b = this.operands[1].eval(env), + temp; + + if (a instanceof tree.Dimension && b instanceof tree.Color) { + if (this.op === '*' || this.op === '+') { + temp = b, b = a, a = temp; + } else { + throw { name: "OperationError", + message: "Can't substract or divide a color from a number" }; + } + } + return a.operate(this.op, b); +}; + +tree.operate = function (op, a, b) { + switch (op) { + case '+': return a + b; + case '-': return a - b; + case '*': return a * b; + case '/': return a / b; + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/paren.js b/dashboard/bin/lib/less/tree/paren.js new file mode 100644 index 00000000..384a43c7 --- /dev/null +++ b/dashboard/bin/lib/less/tree/paren.js @@ -0,0 +1,16 @@ + +(function (tree) { + +tree.Paren = function (node) { + this.value = node; +}; +tree.Paren.prototype = { + toCSS: function (env) { + return '(' + this.value.toCSS(env) + ')'; + }, + eval: function (env) { + return new(tree.Paren)(this.value.eval(env)); + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/quoted.js b/dashboard/bin/lib/less/tree/quoted.js new file mode 100644 index 00000000..794bf4ce --- /dev/null +++ b/dashboard/bin/lib/less/tree/quoted.js @@ -0,0 +1,29 @@ +(function (tree) { + +tree.Quoted = function (str, content, escaped, i) { + this.escaped = escaped; + this.value = content || ''; + this.quote = str.charAt(0); + this.index = i; +}; +tree.Quoted.prototype = { + toCSS: function () { + if (this.escaped) { + return this.value; + } else { + return this.quote + this.value + this.quote; + } + }, + eval: function (env) { + var that = this; + var value = this.value.replace(/`([^`]+)`/g, function (_, exp) { + return new(tree.JavaScript)(exp, that.index, true).eval(env).value; + }).replace(/@\{([\w-]+)\}/g, function (_, name) { + var v = new(tree.Variable)('@' + name, that.index).eval(env); + return ('value' in v) ? v.value : v.toCSS(); + }); + return new(tree.Quoted)(this.quote + value + this.quote, value, this.escaped, this.index); + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/rule.js b/dashboard/bin/lib/less/tree/rule.js new file mode 100644 index 00000000..9e4e54a3 --- /dev/null +++ b/dashboard/bin/lib/less/tree/rule.js @@ -0,0 +1,42 @@ +(function (tree) { + +tree.Rule = function (name, value, important, index, inline) { + this.name = name; + this.value = (value instanceof tree.Value) ? value : new(tree.Value)([value]); + this.important = important ? ' ' + important.trim() : ''; + this.index = index; + this.inline = inline || false; + + if (name.charAt(0) === '@') { + this.variable = true; + } else { this.variable = false } +}; +tree.Rule.prototype.toCSS = function (env) { + if (this.variable) { return "" } + else { + return this.name + (env.compress ? ':' : ': ') + + this.value.toCSS(env) + + this.important + (this.inline ? "" : ";"); + } +}; + +tree.Rule.prototype.eval = function (context) { + return new(tree.Rule)(this.name, + this.value.eval(context), + this.important, + this.index, this.inline); +}; + +tree.Shorthand = function (a, b) { + this.a = a; + this.b = b; +}; + +tree.Shorthand.prototype = { + toCSS: function (env) { + return this.a.toCSS(env) + "/" + this.b.toCSS(env); + }, + eval: function () { return this } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/ruleset.js b/dashboard/bin/lib/less/tree/ruleset.js new file mode 100644 index 00000000..3100cc35 --- /dev/null +++ b/dashboard/bin/lib/less/tree/ruleset.js @@ -0,0 +1,225 @@ +(function (tree) { + +tree.Ruleset = function (selectors, rules, strictImports) { + this.selectors = selectors; + this.rules = rules; + this._lookups = {}; + this.strictImports = strictImports; +}; +tree.Ruleset.prototype = { + eval: function (env) { + var selectors = this.selectors && this.selectors.map(function (s) { return s.eval(env) }); + var ruleset = new(tree.Ruleset)(selectors, this.rules.slice(0), this.strictImports); + + ruleset.root = this.root; + ruleset.allowImports = this.allowImports; + + // push the current ruleset to the frames stack + env.frames.unshift(ruleset); + + // Evaluate imports + if (ruleset.root || ruleset.allowImports || !ruleset.strictImports) { + for (var i = 0; i < ruleset.rules.length; i++) { + if (ruleset.rules[i] instanceof tree.Import) { + Array.prototype.splice + .apply(ruleset.rules, [i, 1].concat(ruleset.rules[i].eval(env))); + } + } + } + + // Store the frames around mixin definitions, + // so they can be evaluated like closures when the time comes. + for (var i = 0; i < ruleset.rules.length; i++) { + if (ruleset.rules[i] instanceof tree.mixin.Definition) { + ruleset.rules[i].frames = env.frames.slice(0); + } + } + + // Evaluate mixin calls. + for (var i = 0; i < ruleset.rules.length; i++) { + if (ruleset.rules[i] instanceof tree.mixin.Call) { + Array.prototype.splice + .apply(ruleset.rules, [i, 1].concat(ruleset.rules[i].eval(env))); + } + } + + // Evaluate everything else + for (var i = 0, rule; i < ruleset.rules.length; i++) { + rule = ruleset.rules[i]; + + if (! (rule instanceof tree.mixin.Definition)) { + ruleset.rules[i] = rule.eval ? rule.eval(env) : rule; + } + } + + // Pop the stack + env.frames.shift(); + + return ruleset; + }, + match: function (args) { + return !args || args.length === 0; + }, + variables: function () { + if (this._variables) { return this._variables } + else { + return this._variables = this.rules.reduce(function (hash, r) { + if (r instanceof tree.Rule && r.variable === true) { + hash[r.name] = r; + } + return hash; + }, {}); + } + }, + variable: function (name) { + return this.variables()[name]; + }, + rulesets: function () { + if (this._rulesets) { return this._rulesets } + else { + return this._rulesets = this.rules.filter(function (r) { + return (r instanceof tree.Ruleset) || (r instanceof tree.mixin.Definition); + }); + } + }, + find: function (selector, self) { + self = self || this; + var rules = [], rule, match, + key = selector.toCSS(); + + if (key in this._lookups) { return this._lookups[key] } + + this.rulesets().forEach(function (rule) { + if (rule !== self) { + for (var j = 0; j < rule.selectors.length; j++) { + if (match = selector.match(rule.selectors[j])) { + if (selector.elements.length > rule.selectors[j].elements.length) { + Array.prototype.push.apply(rules, rule.find( + new(tree.Selector)(selector.elements.slice(1)), self)); + } else { + rules.push(rule); + } + break; + } + } + } + }); + return this._lookups[key] = rules; + }, + // + // Entry point for code generation + // + // `context` holds an array of arrays. + // + toCSS: function (context, env) { + var css = [], // The CSS output + rules = [], // node.Rule instances + _rules = [], // + rulesets = [], // node.Ruleset instances + paths = [], // Current selectors + selector, // The fully rendered selector + rule; + + if (! this.root) { + if (context.length === 0) { + paths = this.selectors.map(function (s) { return [s] }); + } else { + this.joinSelectors(paths, context, this.selectors); + } + } + + // Compile rules and rulesets + for (var i = 0; i < this.rules.length; i++) { + rule = this.rules[i]; + + if (rule.rules || (rule instanceof tree.Directive) || (rule instanceof tree.Media)) { + rulesets.push(rule.toCSS(paths, env)); + } else if (rule instanceof tree.Comment) { + if (!rule.silent) { + if (this.root) { + rulesets.push(rule.toCSS(env)); + } else { + rules.push(rule.toCSS(env)); + } + } + } else { + if (rule.toCSS && !rule.variable) { + rules.push(rule.toCSS(env)); + } else if (rule.value && !rule.variable) { + rules.push(rule.value.toString()); + } + } + } + + rulesets = rulesets.join(''); + + // If this is the root node, we don't render + // a selector, or {}. + // Otherwise, only output if this ruleset has rules. + if (this.root) { + css.push(rules.join(env.compress ? '' : '\n')); + } else { + if (rules.length > 0) { + selector = paths.map(function (p) { + return p.map(function (s) { + return s.toCSS(env); + }).join('').trim(); + }).join(env.compress ? ',' : ',\n'); + + // Remove duplicates + for (var i = rules.length - 1; i >= 0; i--) { + if (_rules.indexOf(rules[i]) === -1) { + _rules.unshift(rules[i]); + } + } + rules = _rules; + + css.push(selector, + (env.compress ? '{' : ' {\n ') + + rules.join(env.compress ? '' : '\n ') + + (env.compress ? '}' : '\n}\n')); + } + } + css.push(rulesets); + + return css.join('') + (env.compress ? '\n' : ''); + }, + + joinSelectors: function (paths, context, selectors) { + for (var s = 0; s < selectors.length; s++) { + this.joinSelector(paths, context, selectors[s]); + } + }, + + joinSelector: function (paths, context, selector) { + var before = [], after = [], beforeElements = [], + afterElements = [], hasParentSelector = false, el; + + for (var i = 0; i < selector.elements.length; i++) { + el = selector.elements[i]; + if (el.combinator.value.charAt(0) === '&') { + hasParentSelector = true; + } + if (hasParentSelector) afterElements.push(el); + else beforeElements.push(el); + } + + if (! hasParentSelector) { + afterElements = beforeElements; + beforeElements = []; + } + + if (beforeElements.length > 0) { + before.push(new(tree.Selector)(beforeElements)); + } + + if (afterElements.length > 0) { + after.push(new(tree.Selector)(afterElements)); + } + + for (var c = 0; c < context.length; c++) { + paths.push(before.concat(context[c]).concat(after)); + } + } +}; +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/selector.js b/dashboard/bin/lib/less/tree/selector.js new file mode 100644 index 00000000..65abbb69 --- /dev/null +++ b/dashboard/bin/lib/less/tree/selector.js @@ -0,0 +1,42 @@ +(function (tree) { + +tree.Selector = function (elements) { + this.elements = elements; + if (this.elements[0].combinator.value === "") { + this.elements[0].combinator.value = ' '; + } +}; +tree.Selector.prototype.match = function (other) { + var len = this.elements.length, + olen = other.elements.length, + max = Math.min(len, olen); + + if (len < olen) { + return false; + } else { + for (var i = 0; i < max; i++) { + if (this.elements[i].value !== other.elements[i].value) { + return false; + } + } + } + return true; +}; +tree.Selector.prototype.eval = function (env) { + return new(tree.Selector)(this.elements.map(function (e) { + return e.eval(env); + })); +}; +tree.Selector.prototype.toCSS = function (env) { + if (this._css) { return this._css } + + return this._css = this.elements.map(function (e) { + if (typeof(e) === 'string') { + return ' ' + e.trim(); + } else { + return e.toCSS(env); + } + }).join(''); +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/url.js b/dashboard/bin/lib/less/tree/url.js new file mode 100644 index 00000000..0caec345 --- /dev/null +++ b/dashboard/bin/lib/less/tree/url.js @@ -0,0 +1,25 @@ +(function (tree) { + +tree.URL = function (val, paths) { + if (val.data) { + this.attrs = val; + } else { + // Add the base path if the URL is relative and we are in the browser + if (typeof(window) !== 'undefined' && !/^(?:https?:\/\/|file:\/\/|data:|\/)/.test(val.value) && paths.length > 0) { + val.value = paths[0] + (val.value.charAt(0) === '/' ? val.value.slice(1) : val.value); + } + this.value = val; + this.paths = paths; + } +}; +tree.URL.prototype = { + toCSS: function () { + return "url(" + (this.attrs ? 'data:' + this.attrs.mime + this.attrs.charset + this.attrs.base64 + this.attrs.data + : this.value.toCSS()) + ")"; + }, + eval: function (ctx) { + return this.attrs ? this : new(tree.URL)(this.value.eval(ctx), this.paths); + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/value.js b/dashboard/bin/lib/less/tree/value.js new file mode 100644 index 00000000..3c1eb29a --- /dev/null +++ b/dashboard/bin/lib/less/tree/value.js @@ -0,0 +1,24 @@ +(function (tree) { + +tree.Value = function (value) { + this.value = value; + this.is = 'value'; +}; +tree.Value.prototype = { + eval: function (env) { + if (this.value.length === 1) { + return this.value[0].eval(env); + } else { + return new(tree.Value)(this.value.map(function (v) { + return v.eval(env); + })); + } + }, + toCSS: function (env) { + return this.value.map(function (e) { + return e.toCSS(env); + }).join(env.compress ? ',' : ', '); + } +}; + +})(require('../tree')); diff --git a/dashboard/bin/lib/less/tree/variable.js b/dashboard/bin/lib/less/tree/variable.js new file mode 100644 index 00000000..ee557e1d --- /dev/null +++ b/dashboard/bin/lib/less/tree/variable.js @@ -0,0 +1,26 @@ +(function (tree) { + +tree.Variable = function (name, index, file) { this.name = name, this.index = index, this.file = file }; +tree.Variable.prototype = { + eval: function (env) { + var variable, v, name = this.name; + + if (name.indexOf('@@') == 0) { + name = '@' + new(tree.Variable)(name.slice(1)).eval(env).value; + } + + if (variable = tree.find(env.frames, function (frame) { + if (v = frame.variable(name)) { + return v.value.eval(env); + } + })) { return variable } + else { + throw { type: 'Name', + message: "variable " + name + " is undefined", + filename: this.file, + index: this.index }; + } + } +}; + +})(require('../tree')); diff --git a/dashboard/doc/Makefile b/dashboard/doc/Makefile new file mode 100644 index 00000000..986ad3df --- /dev/null +++ b/dashboard/doc/Makefile @@ -0,0 +1,153 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Horizon.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Horizon.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Horizon" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Horizon" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/dashboard/windc/__init__.py b/dashboard/doc/source/_static/.gitignore similarity index 100% rename from dashboard/windc/__init__.py rename to dashboard/doc/source/_static/.gitignore diff --git a/dashboard/doc/source/_static/basic.css b/dashboard/doc/source/_static/basic.css new file mode 100644 index 00000000..d909ce37 --- /dev/null +++ b/dashboard/doc/source/_static/basic.css @@ -0,0 +1,416 @@ +/** + * Sphinx stylesheet -- basic theme + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +img { + border: 0; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li div.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable dl, table.indextable dd { + margin-top: 0; + margin-bottom: 0; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +/* -- general body styles --------------------------------------------------- */ + +a.headerlink { + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.field-list ul { + padding-left: 1em; +} + +.first { +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px 7px 0 7px; + background-color: #ffe; + width: 40%; + float: right; +} + +p.sidebar-title { + font-weight: bold; +} + +/* -- topics ---------------------------------------------------------------- */ + +div.topic { + border: 1px solid #ccc; + padding: 7px 7px 0 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +div.admonition dl { + margin-bottom: 0; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + border: 0; + border-collapse: collapse; +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 0; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +table.field-list td, table.field-list th { + border: 0 !important; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +th { + text-align: left; + padding-right: 5px; +} + +/* -- other body styles ----------------------------------------------------- */ + +dl { + margin-bottom: 15px; +} + +dd p { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dt:target, .highlight { + background-color: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.refcount { + color: #060; +} + +.optional { + font-size: 1.3em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; +} + +td.linenos pre { + padding: 5px 0px; + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + margin-left: 0.5em; +} + +table.highlighttable td { + padding: 0 0.5em 0 0.5em; +} + +tt.descname { + background-color: transparent; + font-weight: bold; + font-size: 1.2em; +} + +tt.descclassname { + background-color: transparent; +} + +tt.xref, a tt { + background-color: transparent; + font-weight: bold; +} + +h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { + background-color: transparent; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} diff --git a/dashboard/doc/source/_static/default.css b/dashboard/doc/source/_static/default.css new file mode 100644 index 00000000..c8091ecb --- /dev/null +++ b/dashboard/doc/source/_static/default.css @@ -0,0 +1,230 @@ +/** + * Sphinx stylesheet -- default theme + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: sans-serif; + font-size: 100%; + background-color: #11303d; + color: #000; + margin: 0; + padding: 0; +} + +div.document { + background-color: #1c4e63; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 230px; +} + +div.body { + background-color: #ffffff; + color: #000000; + padding: 0 20px 30px 20px; +} + +div.footer { + color: #ffffff; + width: 100%; + padding: 9px 0 9px 0; + text-align: center; + font-size: 75%; +} + +div.footer a { + color: #ffffff; + text-decoration: underline; +} + +div.related { + background-color: #133f52; + line-height: 30px; + color: #ffffff; +} + +div.related a { + color: #ffffff; +} + +div.sphinxsidebar { +} + +div.sphinxsidebar h3 { + font-family: 'Trebuchet MS', sans-serif; + color: #ffffff; + font-size: 1.4em; + font-weight: normal; + margin: 0; + padding: 0; +} + +div.sphinxsidebar h3 a { + color: #ffffff; +} + +div.sphinxsidebar h4 { + font-family: 'Trebuchet MS', sans-serif; + color: #ffffff; + font-size: 1.3em; + font-weight: normal; + margin: 5px 0 0 0; + padding: 0; +} + +div.sphinxsidebar p { + color: #ffffff; +} + +div.sphinxsidebar p.topless { + margin: 5px 10px 10px 10px; +} + +div.sphinxsidebar ul { + margin: 10px; + padding: 0; + color: #ffffff; +} + +div.sphinxsidebar a { + color: #98dbcc; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #355f7c; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +div.body p, div.body dd, div.body li { + text-align: left; + line-height: 130%; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: 'Trebuchet MS', sans-serif; + background-color: #f2f2f2; + font-weight: normal; + color: #20435c; + border-bottom: 1px solid #ccc; + margin: 20px -20px 10px -20px; + padding: 3px 0 3px 10px; +} + +div.body h1 { margin-top: 0; font-size: 200%; } +div.body h2 { font-size: 160%; } +div.body h3 { font-size: 140%; } +div.body h4 { font-size: 120%; } +div.body h5 { font-size: 110%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: #c60f0f; + font-size: 0.8em; + padding: 0 4px 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + background-color: #c60f0f; + color: white; +} + +div.body p, div.body dd, div.body li { + text-align: left; + line-height: 130%; +} + +div.admonition p.admonition-title + p { + display: inline; +} + +div.admonition p { + margin-bottom: 5px; +} + +div.admonition pre { + margin-bottom: 5px; +} + +div.admonition ul, div.admonition ol { + margin-bottom: 5px; +} + +div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.topic { + background-color: #eee; +} + +div.warning { + background-color: #ffe4e4; + border: 1px solid #f66; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre { + padding: 5px; + background-color: #eeffcc; + color: #333333; + line-height: 120%; + border: 1px solid #ac9; + border-left: none; + border-right: none; +} + +tt { + background-color: #ecf0f3; + padding: 0 1px 0 1px; + font-size: 0.95em; +} + +.warning tt { + background: #efc2c2; +} + +.note tt { + background: #d6d6d6; +} diff --git a/dashboard/doc/source/_static/header-line.gif b/dashboard/doc/source/_static/header-line.gif new file mode 100644 index 0000000000000000000000000000000000000000..3601730e03488b7b5f92dc992d23ad753357c167 GIT binary patch literal 48 zcmZ?wbhEHbWMg1uXkcVG`smgF|Nj+#vM@3*Ff!;c00Bsbfr-7RpY8O^Kn4bD08FwB Aga7~l literal 0 HcmV?d00001 diff --git a/dashboard/doc/source/_static/header_bg.jpg b/dashboard/doc/source/_static/header_bg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f788c41c26481728fa4329c17c87bde36001adc1 GIT binary patch literal 3738 zcmd5-YdDna8vedHnM0NtYi6>>At7O=uyTsZup5R_40A9)aXQa}U(l^=gSg=J*&3mKp$aM0r>UIFDe9Zy(vs} zWf)kqO2Y_n0$>ZQ0D&hY4tWjpY?Ii5?V)h*kc0fz?%ZIj3|{;F8E5l%d0)&*Hx~ulvc_*73u8%R zsVMV~ne!JY);&pWott~QIZYJFTXliYc2};JEU{X7W6;ZPfz;)U;U4#mEuK@K*=SC3BR-m&x9(Nna@>b@%FS34|P^jtsXRb5>z9gtPp;_MI2F3o*k z>csA-?CX4b;~4P-*L$+Mmb|51F)eD*wCc`Jt(9}C${Zo=!Uin=u_yMC^;`X!x$##4 z+~}dkT`NF@Uhw0r+6g_)?e!h8IX+OE^C96>UOsv0GPMD6(kr#ljhXRnA=O>Qj@%iT zqBF7aQ*}BG)h@6r0%#azk!r9yrN6>9dq~>KadV$~cGG?Hjk>~it^5rd#zS4KE*p+4 z;;B)%oBK8PNTs=A)a-z`n?3zJ%+h{`=>ijk4sYKr*>`eN1H`~Lo|Tm!o6qN{S* zeNl=NcpGzD55)XnLC|>g)~w={=c#4*x^;mk4Zo_FOFlffP@!?1`c+TogTVR4kp9-q z`d5cMBzNxk6qjPRK9*WY3uHS=bnm_QJvSMBBS_A#3i=ywsg6^|9rfruW0MhdGwHDO z?1gJRMQVecKE^gV{%uo(b)zl^Hd&vmnwFh88h*-?FJ;y=Hdqvt!K|s<$>xlzR=G4{ zZgGOCF43IXS?62B)w*N&dXt%U8X^Bjx}^%Yf>VFpFoKSGP%k?ems;&&J)|Dx(qtQD zu2tS)<_Qz4#LhBKYkl@Og}G)^5+F4P($Fk>)}{uMVv|;Sz2i4$XJ_WTw*;n>3N805rnXhbC52SC={E3rXRlrs|I6f;o|Cn%eje59{axu9sivy4oYmg=j|fLt3<3 zFce84aNb8GbK;y>RbBu71YBcYKL3@M3N25yoE%BtG z^K!`WTQ|fb-Ysa7T)mEw&4_b)PWYgc!)3W)H+neR9o^f|AXdgY1`gN+pvgzbbk`M z*Ts6${7M`2)9XIPy^MoXTiiP2GTp_OtgWMshnH)M&ZSO0)cet!oWo_0_&hV(0?Qdb zdo(sw{I#{hI`SWPM`N=U^#+MgN-*rZ#J7Cm7Jj89`5ehd_{z&9->Jc7$F(X4)&|`K z5rEgd;@dhi-IzJnSVpMd!Gf_G-QW+ zjVMrIas1)g%)GJ;(=oaK};O^)NYdS1`XR?K_;I7qj zhii5}x^he{U3M+GF+WpYws#=Pt#S9xB_X5QE7W+_rQdwMhukJnQj}5cnCz_sIJ#r0 zJa5drkRPI$X(4YdpCswJe#5aN4Jjw3V3Nzt&`lcKBI~#;!>jq7j8y# zvHrFg_#P376A45^hp-KU*P=R;DVdPK*w7D@Gw+`XsSpm^L-VkCooZF61sPAnnjsT# zND4C{>G#P10F_&txEoE!rX%Iy*L}Kna=Q%fDLJ_rF*LujRITZ)$g!?UYLkCXOoz-S z_p`Hny*Rh--l)aYQC&-2dd%;%VKGC1<1DJm_n~`nk4^yS`}&P zM}5bOypW0hwtvrwnE>}g1Mq+B>09qPp1b$hn6kC_iqF`tX#G-t7D$n}Ky9t}sUqiI zOe@odQ?JueZ+sg`-zoQ}J4if6vv1c9x{BDme+F6z{8esU^Kio zK_oPy9}@nlGywSOZy9`^- zzBg>C9|rgWF{pcCogEV@;d}VHrgeBl=5Dr*th4V!1`Z9Zrz9le1zHC#sM3{j#G2R?WMhl6b_yyoEAxX>Zixl$16`+^d$ihNtuIBUafyiCEv#oksNL<4= z*oDXsc7-(ww^9-b-6_|bITySG1N2C-7p0L4+V@R%j=4@ygc=89bmSNy38$S=ZiDyP z0SrqrVA;zi8kYBZ2@Mx(2Lx~-*bc@d1#4R($RJv$9ZTfx_t7Kc|HIHnd&@I386P?& z?d6Vd(48n${cTNFFCoSIUj#O{mmt%M&xCIFmR9Y3f{2UnF4e9@uFZOaYiY|CLdbDa z%xS9x4SHi7Fr-1?CnDqRK?)n&$TTBW5J?O&o{TnNCnLw*{QmT7{c}flSbp9&xi*zF z1TdUn&_!$_WxQbMKGkgsl}B%+N5ZV%Hy6_zJ>dejD89yCBMw9(d}z2fWjYH_nV6!F zqe_rI2H5Pi0^~S6)jjnu%lqZN*eQq6!||a24+edpSH_{C8Ew^g8dw2qdrH!@*E7K* z)00Bb8uUsai%v6Oa^L@3E02r|EG%EdV>q;=#2Q9Wjv3l?dAur$4bzyOl3M6 z1hf%&o*#2R&xnS1z4&R`Uq%`Ut0_P{BOwt;FuDb$1")); + }); + return $(returning); + }, + linkUser: function() { + var returning = []; + var regexp = /[\@]+([A-Za-z0-9-_]+)/gi; + this.each(function() { + returning.push(this.replace(regexp,"@$1")); + }); + return $(returning); + }, + linkHash: function() { + var returning = []; + var regexp = / [\#]+([A-Za-z0-9-_]+)/gi; + this.each(function() { + returning.push(this.replace(regexp, ' #$1')); + }); + return $(returning); + }, + capAwesome: function() { + var returning = []; + this.each(function() { + returning.push(this.replace(/\b(awesome)\b/gi, '$1')); + }); + return $(returning); + }, + capEpic: function() { + var returning = []; + this.each(function() { + returning.push(this.replace(/\b(epic)\b/gi, '$1')); + }); + return $(returning); + }, + makeHeart: function() { + var returning = []; + this.each(function() { + returning.push(this.replace(/(<)+[3]/gi, "")); + }); + return $(returning); + } + }); + + function relative_time(time_value) { + var parsed_date = Date.parse(time_value); + var relative_to = (arguments.length > 1) ? arguments[1] : new Date(); + var delta = parseInt((relative_to.getTime() - parsed_date) / 1000); + var pluralize = function (singular, n) { + return '' + n + ' ' + singular + (n == 1 ? '' : 's'); + }; + if(delta < 60) { + return 'less than a minute ago'; + } else if(delta < (45*60)) { + return 'about ' + pluralize("minute", parseInt(delta / 60)) + ' ago'; + } else if(delta < (24*60*60)) { + return 'about ' + pluralize("hour", parseInt(delta / 3600)) + ' ago'; + } else { + return 'about ' + pluralize("day", parseInt(delta / 86400)) + ' ago'; + } + } + + function build_url() { + var proto = ('https:' == document.location.protocol ? 'https:' : 'http:'); + if (s.list) { + return proto+"//api.twitter.com/1/"+s.username[0]+"/lists/"+s.list+"/statuses.json?per_page="+s.count+"&callback=?"; + } else if (s.query == null && s.username.length == 1) { + return proto+'//twitter.com/status/user_timeline/'+s.username[0]+'.json?count='+s.count+'&callback=?'; + } else { + var query = (s.query || 'from:'+s.username.join('%20OR%20from:')); + return proto+'//search.twitter.com/search.json?&q='+query+'&rpp='+s.count+'&callback=?'; + } + } + + return this.each(function(){ + var list = $('
      ').appendTo(this); + var intro = '

      '+s.intro_text+'

      '; + var outro = '

      '+s.outro_text+'

      '; + var loading = $('

      '+s.loading_text+'

      '); + + if(typeof(s.username) == "string"){ + s.username = [s.username]; + } + + if (s.loading_text) $(this).append(loading); + $.getJSON(build_url(), function(data){ + if (s.loading_text) loading.remove(); + if (s.intro_text) list.before(intro); + $.each((data.results || data), function(i,item){ + // auto join text based on verb tense and content + if (s.join_text == "auto") { + if (item.text.match(/^(@([A-Za-z0-9-_]+)) .*/i)) { + var join_text = s.auto_join_text_reply; + } else if (item.text.match(/(^\w+:\/\/[A-Za-z0-9-_]+\.[A-Za-z0-9-_:%&\?\/.=]+) .*/i)) { + var join_text = s.auto_join_text_url; + } else if (item.text.match(/^((\w+ed)|just) .*/im)) { + var join_text = s.auto_join_text_ed; + } else if (item.text.match(/^(\w*ing) .*/i)) { + var join_text = s.auto_join_text_ing; + } else { + var join_text = s.auto_join_text_default; + } + } else { + var join_text = s.join_text; + }; + + var from_user = item.from_user || item.user.screen_name; + var profile_image_url = item.profile_image_url || item.user.profile_image_url; + var join_template = ' '+join_text+' '; + var join = ((s.join_text) ? join_template : ' '); + var avatar_template = ''+from_user+'\'s avatar'; + var avatar = (s.avatar_size ? avatar_template : ''); + var date = ''+relative_time(item.created_at)+''; + var text = '' +$([item.text]).linkUrl().linkUser().linkHash().makeHeart().capAwesome().capEpic()[0]+ ''; + + // until we create a template option, arrange the items below to alter a tweet's display. + list.append('
    • ' + avatar + date + join + text + '
    • '); + + list.children('li:first').addClass('tweet_first'); + list.children('li:odd').addClass('tweet_even'); + list.children('li:even').addClass('tweet_odd'); + }); + if (s.outro_text) list.after(outro); + }); + + }); + }; +})(jQuery); \ No newline at end of file diff --git a/dashboard/doc/source/_static/nature.css b/dashboard/doc/source/_static/nature.css new file mode 100644 index 00000000..a98bd420 --- /dev/null +++ b/dashboard/doc/source/_static/nature.css @@ -0,0 +1,245 @@ +/* + * nature.css_t + * ~~~~~~~~~~~~ + * + * Sphinx stylesheet -- nature theme. + * + * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: Arial, sans-serif; + font-size: 100%; + background-color: #111; + color: #555; + margin: 0; + padding: 0; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 {{ theme_sidebarwidth|toint }}px; +} + +hr { + border: 1px solid #B1B4B6; +} + +div.document { + background-color: #eee; +} + +div.body { + background-color: #ffffff; + color: #3E4349; + padding: 0 30px 30px 30px; + font-size: 0.9em; +} + +div.footer { + color: #555; + width: 100%; + padding: 13px 0; + text-align: center; + font-size: 75%; +} + +div.footer a { + color: #444; + text-decoration: underline; +} + +div.related { + background-color: #6BA81E; + line-height: 32px; + color: #fff; + text-shadow: 0px 1px 0 #444; + font-size: 0.9em; +} + +div.related a { + color: #E2F3CC; +} + +div.sphinxsidebar { + font-size: 0.75em; + line-height: 1.5em; +} + +div.sphinxsidebarwrapper{ + padding: 20px 0; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: Arial, sans-serif; + color: #222; + font-size: 1.2em; + font-weight: normal; + margin: 0; + padding: 5px 10px; + background-color: #ddd; + text-shadow: 1px 1px 0 white +} + +div.sphinxsidebar h4{ + font-size: 1.1em; +} + +div.sphinxsidebar h3 a { + color: #444; +} + + +div.sphinxsidebar p { + color: #888; + padding: 5px 20px; +} + +div.sphinxsidebar p.topless { +} + +div.sphinxsidebar ul { + margin: 10px 20px; + padding: 0; + color: #000; +} + +div.sphinxsidebar a { + color: #444; +} + +div.sphinxsidebar input { + border: 1px solid #ccc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar input[type=text]{ + margin-left: 20px; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #005B81; + text-decoration: none; +} + +a:hover { + color: #E32E00; + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: Arial, sans-serif; + background-color: #BED4EB; + font-weight: normal; + color: #212224; + margin: 30px 0px 10px 0px; + padding: 5px 0 5px 10px; + text-shadow: 0px 1px 0 white +} + +div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; } +div.body h2 { font-size: 150%; background-color: #C8D5E3; } +div.body h3 { font-size: 120%; background-color: #D8DEE3; } +div.body h4 { font-size: 110%; background-color: #D8DEE3; } +div.body h5 { font-size: 100%; background-color: #D8DEE3; } +div.body h6 { font-size: 100%; background-color: #D8DEE3; } + +a.headerlink { + color: #c60f0f; + font-size: 0.8em; + padding: 0 4px 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + background-color: #c60f0f; + color: white; +} + +div.body p, div.body dd, div.body li { + line-height: 1.5em; +} + +div.admonition p.admonition-title + p { + display: inline; +} + +div.highlight{ + background-color: white; +} + +div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.topic { + background-color: #eee; +} + +div.warning { + background-color: #ffe4e4; + border: 1px solid #f66; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre { + padding: 10px; + background-color: White; + color: #222; + line-height: 1.2em; + border: 1px solid #C6C9CB; + font-size: 1.1em; + margin: 1.5em 0 1.5em 0; + -webkit-box-shadow: 1px 1px 1px #d8d8d8; + -moz-box-shadow: 1px 1px 1px #d8d8d8; +} + +tt { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ + font-size: 1.1em; + font-family: monospace; +} + +.viewcode-back { + font-family: Arial, sans-serif; +} + +div.viewcode-block:target { + background-color: #f4debf; + border-top: 1px solid #ac9; + border-bottom: 1px solid #ac9; +} diff --git a/dashboard/doc/source/_static/openstack_logo.png b/dashboard/doc/source/_static/openstack_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..146faec5cfe3773824f4caf39e4480e4974d10df GIT binary patch literal 3670 zcmV-c4yo~pP)CW75Qp#l)U;+N6jaIz6Nf$t6dNV>^>ETzcpQ=%tMaf0k|rg72+IW`z$FyfE+D{1@tt$t5DmX)*;QV?c;%+5Z&egAgfXTQJq-mZkC z>pFAHu}U=Axde_?s!99ZfDg_+9TYzDa6N1R3adhx&2Mb7>9w`KpMNz!>U5t2XQ8lZ zu+!+H7(PRwF@jAkwvI;|8|=Z_dfzV`Kpi;I!e=|Ql+HAdEag?VZ^Ilw9XJj9N1#1a z?UFC!)X62`CRIe^9YCLKbJ` z&O@f0zt{Z1YDF1utg2$F+rzvrncys+g37Xsd8)idSW(=}t#~qF#qBo29*@^ZCs<$W zpa144=o4g0z63h_ttPfIpH-FyG^MAH+6B~r$(4qw+Uv{2d#h`$lq+i+#Tf%CAzDFUh!pzX(6nW{EASJAQkhm!+}aGpHc z;(+N`S*@tYmump1T37E}J;!$0#F>^M*mT_X1x~bvnp&qP9IHI#bj-0z8FR+=p+e#*w3ugV#wX``sR-CI1!YiQsfc@Om<;1MBw zlfqH9z4Q|m*C?URU1OG(`UYn>Q8<|I!mby#FlN5MMFE8;Pyh$skbR?ngFLt?%nWSkS-#W5umy>@^DyAERP~{E&`M%0(qi&((^ahqL}u^jT<2dcf)p< z%Fxc9J$nh_`>_oNYC?oy`rIDY46Yrw4si3Qn~oXV%dJ}IlUD-40>QipyGa_dV0Z%J ztcEXm5yxR0gySJ04{nnbm#vP=Hq&GI<8VxcZ34pRjt6m%pE2H|!+HBJQrdBdyKHJR z2O_}hp!5bXuwniQYTF>yI|=cjT+2l`9T3|H+l4%ryPxWQm(ODW#8Ctj_CplcO=)qj zD#d~V6BahR9NY1kE5rF)_j<|!Cqnpq0uOKhL%w z>y8OyeTM1?REXc{0|3b=#WPZneh80PxL=Ljau1~+CgtMgg-vccMDX-L z9^7An_;!lFAi`#G_1F*OdM|Z$EVQs0m0$?mY}(baOZ%Zpd62#Pyg!3Jd4d zD^8+lSir&T6Y9-p9L#Wz6$5nXLjdOl?7Lv!TeMr}F14ranauW9=L>ubu*x>Bcrgwp zjrT@{rL*2Fc}Ilwn07QvdJfMOO2=(1Px)6&ih7lg839!Bx&}lQER~T`^7_x@fXo({ zCZMeZYt*!VgMTg>PR)PBaIwubzRY%jjE`-s zG;B}>2!lD=QLOTfQOEZKIEz*;yTJ9(Af0zNv;IDq7#Fr#W{Ap+7Sq1N3TL21X|h2t z=Dk>^bGSsRX-u+cZ23mMB_Ioc0yNIfcfLWB>$hVU3W3>d&a?IM+bGRGt+t}aiv(eh z(D6Z9N>U2|Qxle(!UVTeEKE6W))3WI5z48Rs8d5v0GwmyC8iQiUJO8KS?QwHl2abL zNW+hadDdPc8z%MSOG$l&WR@!!&M{WLmrnS=-0G#&`a)chX>mN9W1>|yqve@lL8a`f zXRmn$B8P=dLxE!2rIi}a*gh%FI4j?C;b@L=WgypiTRf==n6DKr9mUExo6a@{wLM-I z9%V9{!;5G!<8fMYikfEbrGXRQN-9*24}kIIpP&dEg@fiLqAY5|jjv}$P3x0avZODU zdX`c|G>h`1f=3uEu)L9C)H5%frni#HZXcX`TD{iQ-e2qXxj_f%|WW;byDMc%7+uBy}Y?KLC?jp%yyyeBNkqQ-*osw2ex&97Q{#C7%CdSDMNIV zTdC(LEm?&qPcNOjM)h9Grs|M(gsuhV8@96?m4WkQ>j{bJIs)m^neL%ua!i+N8>Lh+ zKu#7rF~VOH@hb{zGXYwys!Um4Vkf+H8Hj6?^eI%kT%j+HA0K=6qdQ@nfR57Q`Jm9T zc)Yg9-`e~BRE!xoKZ z=mP|0Kihr}V1$5sHw$QekmoL)lQ;~@H$S)}s3xuwypiubB?1%OyBpwC08TH!=?BrQ zhOp`PTu;%u0}Q=XKGb7d$g8*;de8c1UI|Re2R;;Radh_D!FIZg+JP`oJg>5 z;&B7eVAomZe>j~hOOIVRO_Q7eSGz37hxmnsG!n%HX`C6gSqFcg(RLmikn%EPR*wel zrsc;>!vQ<>2ZW`lk`MbNLopFd#_9mh8iKPH;KbjC@xJU${pdxuTF{uO(eG#9t*>XP z_4Seh`r_#q$^xeiuy(=eSouv66cpS!t3n`|j`6xnmSs1q@;0!I)m<6eYHHGMRdB87 ziruozT=gn@yp`B9oGxD-b7PqhZum|oJCfLB38&8v51ijj-Pb`qvCr3FtJ0aFms2h3(n0-}3jJ~J$ zCzep7-MIZFbo$(m8zWm?SoRl__blLE+!fFBVVk1&XLg+vmVNcTk9O2+q?x#F0LZUN zu6oM~C)(7^0|az4nM}@aZf<@RkH0CR8<-Yn-fZe+Dbr#iJWSt#tnR4^h<@ePXWmeHIO4q^X zCbiy(=k3R1o1}0E+7x*OOe-qnIXG{#N_rqK*1NH}Qz6aumTR`YTgo5K=q=61;5@b- zrgUA_Qz=)(TPN!tCZE|{?B0*r9ov5Fcip6xQ2;Yqs*2_o7TFKGp0|~bcP@6+a(rz^ zXXmmyBfT}ucw_t(6s+f^t_)nc>RKW<-q_&J35vN+RPLsR?VAsQeHLyCR7AWvxFOVc zAg-xl=j*RipzaKWx3lAf?ei`PoM;bbAL>svH?JqQwjSulb9bghytRt%*5x-no>xlf zh7qj0LYRXVDU})?Btsy7^71*ujsEP_ACyd)P)*ULWBCXox@PUfwmQ#)Vl&oeIqpQY zHMgU+xe0EhQ)RmjdB3JHGdrsvJ9?A=WwOrn)J?BH{+D&O_@SKdrj2|8Z{hS1T(k>&Zlt;p=tqw*mVY1aLt=u^eAHkW>8cb#@q& z4-SLa@ii zCt7NGrLv)1Scy9ew-sOwwLYn2a6T#KzJgnbacm7Z20q6tcs~C!0DI+r(=$l+x{=W0A}~0&W)ll4*&oF07*qoM6N<$f~n6U7ytkO literal 0 HcmV?d00001 diff --git a/dashboard/doc/source/_static/tweaks.css b/dashboard/doc/source/_static/tweaks.css new file mode 100644 index 00000000..3f3fb3f0 --- /dev/null +++ b/dashboard/doc/source/_static/tweaks.css @@ -0,0 +1,94 @@ +body { + background: #fff url(../_static/header_bg.jpg) top left no-repeat; +} + +#header { + width: 950px; + margin: 0 auto; + height: 102px; +} + +#header h1#logo { + background: url(../_static/openstack_logo.png) top left no-repeat; + display: block; + float: left; + text-indent: -9999px; + width: 175px; + height: 55px; +} + +#navigation { + background: url(../_static/header-line.gif) repeat-x 0 bottom; + display: block; + float: left; + margin: 27px 0 0 25px; + padding: 0; +} + +#navigation li{ + float: left; + display: block; + margin-right: 25px; +} + +#navigation li a { + display: block; + font-weight: normal; + text-decoration: none; + background-position: 50% 0; + padding: 20px 0 5px; + color: #353535; + font-size: 14px; +} + +#navigation li a.current, #navigation li a.section { + border-bottom: 3px solid #cf2f19; + color: #cf2f19; +} + +div.related { + background-color: #cde2f8; + border: 1px solid #b0d3f8; +} + +div.related a { + color: #4078ba; + text-shadow: none; +} + +div.sphinxsidebarwrapper { + padding-top: 0; +} + +pre { + color: #555; +} + +div.documentwrapper h1, div.documentwrapper h2, div.documentwrapper h3, div.documentwrapper h4, div.documentwrapper h5, div.documentwrapper h6 { + font-family: 'PT Sans', sans-serif !important; + color: #264D69; + border-bottom: 1px dotted #C5E2EA; + padding: 0; + background: none; + padding-bottom: 5px; +} + +div.documentwrapper h3 { + color: #CF2F19; +} + +a.headerlink { + color: #fff !important; + margin-left: 5px; + background: #CF2F19 !important; +} + +div.body { + margin-top: -25px; + margin-left: 230px; +} + +div.document { + width: 960px; + margin: 0 auto; +} \ No newline at end of file diff --git a/dashboard/doc/source/_templates/.placeholder b/dashboard/doc/source/_templates/.placeholder new file mode 100644 index 00000000..e69de29b diff --git a/dashboard/doc/source/_theme/layout.html b/dashboard/doc/source/_theme/layout.html new file mode 100644 index 00000000..750b7822 --- /dev/null +++ b/dashboard/doc/source/_theme/layout.html @@ -0,0 +1,83 @@ +{% extends "basic/layout.html" %} +{% set css_files = css_files + ['_static/tweaks.css'] %} +{% set script_files = script_files + ['_static/jquery.tweet.js'] %} + +{%- macro sidebar() %} + {%- if not embedded %}{% if not theme_nosidebar|tobool %} +
      +
      + {%- block sidebarlogo %} + {%- if logo %} + + {%- endif %} + {%- endblock %} + {%- block sidebartoc %} + {%- if display_toc %} +

      {{ _('Table Of Contents') }}

      + {{ toc }} + {%- endif %} + {%- endblock %} + {%- block sidebarrel %} + {%- if prev %} +

      {{ _('Previous topic') }}

      +

      {{ prev.title }}

      + {%- endif %} + {%- if next %} +

      {{ _('Next topic') }}

      +

      {{ next.title }}

      + {%- endif %} + {%- endblock %} + {%- block sidebarsourcelink %} + {%- if show_source and has_source and sourcename %} +

      {{ _('This Page') }}

      + + {%- endif %} + {%- endblock %} + {%- if customsidebar %} + {% include customsidebar %} + {%- endif %} + {%- block sidebarsearch %} + {%- if pagename != "search" %} + + + {%- endif %} + {%- endblock %} +
      +
      + {%- endif %}{% endif %} +{%- endmacro %} + +{% block relbar1 %}{% endblock relbar1 %} + +{% block header %} + +{% endblock %} \ No newline at end of file diff --git a/dashboard/doc/source/_theme/theme.conf b/dashboard/doc/source/_theme/theme.conf new file mode 100644 index 00000000..1cc40044 --- /dev/null +++ b/dashboard/doc/source/_theme/theme.conf @@ -0,0 +1,4 @@ +[theme] +inherit = basic +stylesheet = nature.css +pygments_style = tango diff --git a/dashboard/doc/source/conf.py b/dashboard/doc/source/conf.py new file mode 100644 index 00000000..561b4407 --- /dev/null +++ b/dashboard/doc/source/conf.py @@ -0,0 +1,427 @@ +# -*- coding: utf-8 -*- +# +# Horizon documentation build configuration file, created by +# sphinx-quickstart on Thu Oct 27 11:38:59 2011. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", "..")) + +sys.path.insert(0, ROOT) + +# This is required for ReadTheDocs.org, but isn't a bad idea anyway. +os.environ['DJANGO_SETTINGS_MODULE'] = 'openstack_dashboard.settings' + +import horizon.version + + +def write_autodoc_index(): + + def find_autodoc_modules(module_name, sourcedir): + """returns a list of modules in the SOURCE directory""" + modlist = [] + os.chdir(os.path.join(sourcedir, module_name)) + print "SEARCHING %s" % sourcedir + for root, dirs, files in os.walk("."): + for filename in files: + if filename.endswith(".py"): + # remove the pieces of the root + elements = root.split(os.path.sep) + # replace the leading "." with the module name + elements[0] = module_name + # and get the base module name + base, extension = os.path.splitext(filename) + if not (base == "__init__"): + elements.append(base) + result = ".".join(elements) + #print result + modlist.append(result) + return modlist + + RSTDIR = os.path.abspath(os.path.join(BASE_DIR, "sourcecode")) + SRCS = {'horizon': ROOT, + 'openstack_dashboard': ROOT} + + EXCLUDED_MODULES = ('horizon.tests', 'openstack_dashboard.tests',) + CURRENT_SOURCES = {} + + if not(os.path.exists(RSTDIR)): + os.mkdir(RSTDIR) + CURRENT_SOURCES[RSTDIR] = ['autoindex.rst'] + + INDEXOUT = open(os.path.join(RSTDIR, "autoindex.rst"), "w") + INDEXOUT.write("=================\n") + INDEXOUT.write("Source Code Index\n") + INDEXOUT.write("=================\n") + + for modulename, path in SRCS.items(): + sys.stdout.write("Generating source documentation for %s\n" % + modulename) + INDEXOUT.write("\n%s\n" % modulename.capitalize()) + INDEXOUT.write("%s\n" % ("=" * len(modulename),)) + INDEXOUT.write(".. toctree::\n") + INDEXOUT.write(" :maxdepth: 1\n") + INDEXOUT.write("\n") + + MOD_DIR = os.path.join(RSTDIR, modulename) + CURRENT_SOURCES[MOD_DIR] = [] + if not(os.path.exists(MOD_DIR)): + os.mkdir(MOD_DIR) + for module in find_autodoc_modules(modulename, path): + if any([module.startswith(exclude) for exclude \ + in EXCLUDED_MODULES]): + print "Excluded module %s." % module + continue + mod_path = os.path.join(path, *module.split(".")) + generated_file = os.path.join(MOD_DIR, "%s.rst" % module) + + INDEXOUT.write(" %s/%s\n" % (modulename, module)) + + # Find the __init__.py module if this is a directory + if os.path.isdir(mod_path): + source_file = ".".join((os.path.join(mod_path, "__init__"), + "py",)) + else: + source_file = ".".join((os.path.join(mod_path), "py")) + + CURRENT_SOURCES[MOD_DIR].append("%s.rst" % module) + # Only generate a new file if the source has changed or we don't + # have a doc file to begin with. + if not os.access(generated_file, os.F_OK) or \ + os.stat(generated_file).st_mtime < \ + os.stat(source_file).st_mtime: + print "Module %s updated, generating new documentation." \ + % module + FILEOUT = open(generated_file, "w") + header = "The :mod:`%s` Module" % module + FILEOUT.write("%s\n" % ("=" * len(header),)) + FILEOUT.write("%s\n" % header) + FILEOUT.write("%s\n" % ("=" * len(header),)) + FILEOUT.write(".. automodule:: %s\n" % module) + FILEOUT.write(" :members:\n") + FILEOUT.write(" :undoc-members:\n") + FILEOUT.write(" :show-inheritance:\n") + FILEOUT.write(" :noindex:\n") + FILEOUT.close() + + INDEXOUT.close() + + # Delete auto-generated .rst files for sources which no longer exist + for directory, subdirs, files in list(os.walk(RSTDIR)): + for old_file in files: + if old_file not in CURRENT_SOURCES.get(directory, []): + print "Removing outdated file for %s" % old_file + os.remove(os.path.join(directory, old_file)) + + +write_autodoc_index() + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ---------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# 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', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.pngmath', + 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +if os.getenv('HUDSON_PUBLISH_DOCS'): + templates_path = ['_ga', '_templates'] +else: + templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Horizon' +copyright = u'2012, OpenStack, LLC' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = horizon.version.version_info.version_string() +# The full version, including alpha/beta/rc tags. +release = horizon.version.version_info.release_string() + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['**/#*', '**~', '**/#*#'] + +# The reST default role (used for this markup: `text`) +# to use for all documents. +#default_role = None + +# 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 + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +primary_domain = 'py' +nitpicky = False + + +# -- Options for HTML output -------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme_path = ['.'] +html_theme = '_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +html_theme_options = { + "nosidebar": "false" +} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' +git_cmd = "git log --pretty=format:'%ad, commit %h' --date=local -n1" +html_last_updated_fmt = os.popen(git_cmd).read() + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Horizondoc' + + +# -- Options for LaTeX output ------------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto/manual]). +latex_documents = [ + ('index', 'Horizon.tex', u'Horizon Documentation', + u'OpenStack, LLC', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output ------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'horizon', u'Horizon Documentation', + [u'OpenStack'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ----------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'Horizon', u'Horizon Documentation', u'OpenStack', + 'Horizon', 'One line description of project.', 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + + +# -- Options for Epub output -------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = u'Horizon' +epub_author = u'OpenStack' +epub_publisher = u'OpenStack' +epub_copyright = u'2012, OpenStack' + +# The language of the text. It defaults to the language option +# or en if the language is not set. +#epub_language = '' + +# The scheme of the identifier. Typical schemes are ISBN or URL. +#epub_scheme = '' + +# The unique identifier of the text. This can be an ISBN number +# or the project homepage. +#epub_identifier = '' + +# A unique identification for the text. +#epub_uid = '' + +# A tuple containing the cover image and cover page html template filenames. +#epub_cover = () + +# HTML files that should be inserted before the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_pre_files = [] + +# HTML files shat should be inserted after the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_post_files = [] + +# A list of files that should not be packed into the epub file. +#epub_exclude_files = [] + +# The depth of the table of contents in toc.ncx. +#epub_tocdepth = 3 + +# Allow duplicate toc entries. +#epub_tocdup = True + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'python': ('http://docs.python.org/', None), + 'django': + ('http://docs.djangoproject.com/en/dev/_objects/'), + 'nova': ('http://nova.openstack.org', None), + 'swift': ('http://swift.openstack.org', None), + 'keystone': ('http://keystone.openstack.org', None), + 'glance': ('http://glance.openstack.org', None)} diff --git a/dashboard/doc/source/contributing.rst b/dashboard/doc/source/contributing.rst new file mode 100644 index 00000000..e884d854 --- /dev/null +++ b/dashboard/doc/source/contributing.rst @@ -0,0 +1,204 @@ +================== +Contributing Guide +================== + +First and foremost, thank you for wanting to contribute! It's the only way +open source works! + +Before you dive into writing patches, here are some of the basics: + +* Project page: http://launchpad.net/horizon +* Bug tracker: https://bugs.launchpad.net/horizon +* Source code: https://github.com/openstack/horizon +* Code review: https://review.openstack.org/#q,status:open+project:openstack/horizon,n,z +* Jenkins build status: https://jenkins.openstack.org/view/Horizon/ +* IRC Channel: #openstack-horizon on Freenode. + +Making Contributions +==================== + +Getting Started +--------------- + +We'll start by assuming you've got a working checkout of the repository (if +not then please see the :doc:`quickstart`). + +Second, you'll need to take care of a couple administrative tasks: + +#. Create an account on Launchpad. +#. Sign the `OpenStack Contributor License Agreement`_ and follow the associated + instructions to verify your signature. +#. Request to join the `OpenStack Contributors`_ team on Launchpad. +#. Join the `Horizon Developers`_ team on Launchpad. +#. Follow the `instructions for setting up git-review`_ in your + development environment. + +Whew! Got that all that? Okay! You're good to go. + +Ways To Contribute +------------------ + +The easiest way to get started with Horizon's code is to pick a bug on +Launchpad that interests you, and start working on that. Alternatively, if +there's an OpenStack API feature you would like to see implemented in Horizon +feel free to try building it. + +If those are too big, there are lots of great ways to get involved without +plunging in head-first: + +* Report bugs, triage new tickets, and review old tickets on + the `bug tracker`_. +* Propose ideas for improvements via Launchpad Blueprints, via the + mailing list on the project page, or on IRC. +* Write documentation! +* Write unit tests for untested code! + +.. _`bug tracker`: https://bugs.launchpad.net/horizon + +Choosing Issues To Work On +-------------------------- + +In general, if you want to write code, there are three cases for issues +you might want to work on: + +#. Confirmed bugs +#. Approved blueprints (features) +#. New bugs you've discovered + +If you have an idea for a new feature that isn't in a blueprint yet, it's +a good idea to write the blueprint first so you don't end up writing a bunch +of code that may not go in the direction the community wants. + +For bugs, open the bug first, but if you can reproduce the bug reliably and +identify its cause then it's usually safe to start working on it. However, +getting independent confirmation (and verifying that it's not a duplicate) +is always a good idea if you can be patient. + +After You Write Your Patch +-------------------------- + +Once you've made your changes, there are a few things to do: + +* Make sure the unit tests pass: ``./run_tests.sh`` +* Make sure PEP8 is clean: ``./run_tests.sh --pep8`` +* Make sure your code is up-to-date with the latest master: ``git pull --rebase`` +* Finally, run ``git review`` to upload your changes to Gerrit for review. + +The Horizon core developers will be notified of the new review and will examine +it in a timely fashion, either offering feedback or approving it to be merged. +If the review is approved, it is sent to Jenkins to verify the unit tests pass +and it can be merged cleanly. Once Jenkins approves it, the change will be +merged to the master repository and it's time to celebrate! + +.. _`OpenStack Contributor License Agreement`: http://wiki.openstack.org/CLA +.. _`OpenStack Contributors`: https://launchpad.net/~openstack-cla +.. _`Horizon Developers`: https://launchpad.net/~horizon +.. _`instructions for setting up git-review`: http://wiki.openstack.org/GerritWorkflow + +Etiquette +========= + +The community's guidelines for etiquette are fairly simple: + +* Treat everyone respectfully and professionally. +* If a bug is "in progress" in the bug tracker, don't start working on it + without contacting the author. Try on IRC, or via the launchpad email + contact link. If you don't get a response after a reasonable time, then go + ahead. Checking first avoids duplicate work and makes sure nobody's toes + get stepped on. +* If a blueprint is assigned, even if it hasn't been started, be sure you + contact the assignee before taking it on. These larger issues often have a + history of discussion or specific implementation details that the assignee + may be aware of that you are not. +* Please don't re-open tickets closed by a core developer. If you disagree with + the decision on the ticket, the appropriate solution is to take it up on + IRC or the mailing list. +* Give credit where credit is due; if someone helps you substantially with + a piece of code, it's polite (though not required) to thank them in your + commit message. + +Code Style +========== + +Python +------ + +We follow PEP8_ for all our Python code, and use ``pep8.py`` (available +via the shortcut ``./run_tests.sh --pep8``) to validate that our code +meets proper Python style guidelines. + +.. _PEP8: http://www.python.org/dev/peps/pep-0008/ + +Django +------ + +Additionally, we follow `Django's style guide`_ for templates, views, and +other miscellany. + +.. _Django's style guide: https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/ + +JavaScript +---------- + +As a project, Horizon adheres to code quality standards for our JavaScript +just as we do for our Python. To that end we recommend (but do not strictly +enforce) the use of JSLint_ to validate some general best practices. + +The default options are mostly good, but the following accommodate some +allowances we make: + +* Set ``Indentation`` to ``2``. +* Enable the ``Assume console, alert, ...`` option. +* Enable the ``Assume a browser`` option. +* Enable the ``Tolerate missing 'use strict' pragma`` option. +* Clear the ``Maximum number of errors`` field. +* Add ``horizon,$`` to the ``Predefined`` list. + +.. _JSLint: http://jslint.com/ + +CSS +--- + +Style guidelines for CSS are currently quite minimal. Do your best to make the +code readable and well-organized. Two spaces are preferred for indentation +so as to match both the JavaScript and HTML files. + +HTML +---- + +Again, readability is paramount; however be conscientous of how the browser +will handle whitespace when rendering the output. Two spaces is the preferred +indentation style to match all front-end code. + +Documentation +------------- + +Horizon's documentation is written in reStructuredText and uses Sphinx for +additional parsing and functionality, and should follow +standard practices for writing reST. This includes: + +* Flow paragraphs such that lines wrap at 80 characters or less. +* Use proper grammar, spelling, capitalization and punctuation at all times. +* Make use of Sphinx's autodoc feature to document modules, classes + and functions. This keeps the docs close to the source. +* Where possible, use Sphinx's cross-reference syntax (e.g. + ``:class:`~horizon.foo.Bar```) when referring to other Horizon components. + The better-linked our docs are, the easier they are to use. + +Be sure to generate the documentation before submitting a patch for review. +Unexpected warnings often appear when building the documentation, and slight +reST syntax errors frequently cause links or cross-references not to work +correctly. + +Conventions +----------- + +Simply by convention, we have a few rules about naming: + + * The term "project" is used in place of Keystone's "tenant" terminology + in all user-facing text. The term "tenant" is still used in API code to + make things more obvious for developers. + + * The term "dashboard" refers to a top-level dashboard class, and "panel" to + the sub-items within a dashboard. Referring to a panel as a dashboard is + both confusing and incorrect. diff --git a/dashboard/doc/source/faq.rst b/dashboard/doc/source/faq.rst new file mode 100644 index 00000000..26836ef6 --- /dev/null +++ b/dashboard/doc/source/faq.rst @@ -0,0 +1,37 @@ +========================== +Frequently Asked Questions +========================== + +What is the relationship between ``Dashboards``, ``Panels``, and navigation? + + The navigational structure is strongly encouraged to flow from + ``Dashboard`` objects as top-level navigation items to ``Panel`` objects as + sub-navigation items as in the current implementation. Template tags + are provided to automatically generate this structure. + + That said, you are not required to use the provided tools and can write + templates and URLconfs by hand to create any desired structure. + +Does a panel have to be an app in ``INSTALLED_APPS``? + + A panel can live in any Python module. It can be a standalone which ties + into an existing dashboard, or it can be contained alongside others within + a larger dashboard "app". There is no strict enforcement here. Python + is "a language for consenting adults." A module containing a Panel does + not need to be added to ``INSTALLED_APPS``, but this is a common and + convenient way to load a standalone panel. + +Could I hook an external service into a panel using, for example, an iFrame? + + Panels are just entry-points to hook views into the larger dashboard + navigational structure and enforce common attributes like RBAC. The + view and corresponding templates can contain anything you would like, + including iFrames. + +What does this mean for visual design? + + The ability to add an arbitrary number of top-level navigational items + (``Dashboard`` objects) poses a new design challenge. Horizon's lead + designer has taken on the challenge of providing a reference design + for Horizon which supports this possibility. + diff --git a/dashboard/doc/source/glossary.rst b/dashboard/doc/source/glossary.rst new file mode 100644 index 00000000..36bf635e --- /dev/null +++ b/dashboard/doc/source/glossary.rst @@ -0,0 +1,24 @@ +======== +Glossary +======== + +Horizon + + The OpenStack dashboard project. Also the name of the top-level + Python object which handles registration for the app. + +Dashboard + + A Python class representing a top-level navigation item (e.g. "syspanel") + which provides a consistent API for Horizon-compatible applications. + +Panel + + A Python class representing a sub-navigation item (e.g. "instances") + which contains all the necessary logic (views, forms, tests, etc.) for + that interface. + +Project + + Used in user-facing text in place of the term "Tenant" which is Keystone's + word. diff --git a/dashboard/doc/source/index.rst b/dashboard/doc/source/index.rst new file mode 100644 index 00000000..de5feae5 --- /dev/null +++ b/dashboard/doc/source/index.rst @@ -0,0 +1,126 @@ +.. + Copyright 2012 OpenStack, LLC + All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); you may + not use this file except in compliance with the License. You may obtain + a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + License for the specific language governing permissions and limitations + under the License. + +======================================== +Horizon: The OpenStack Dashboard Project +======================================== + +Introduction +============ + +Horizon is the canonical implementation of `Openstack's Dashboard +`_, which provides a web based user +interface to OpenStack services including Nova, Swift, Keystone, etc. + +For a more in-depth look at Horizon and its architecture, see the +:doc:`Introduction to Horizon `. + +To learn what you need to know to get going, see the :doc:`quickstart`. + +Getting Started With Horizon +============================ + +How to use Horizon in your own projects. + +.. toctree:: + :maxdepth: 1 + + intro + quickstart + topics/tutorial + topics/deployment + topics/customizing + +Developer Docs +============== + +For those wishing to develop Horizon itself, or go in-depth with building +your own :class:`~horizon.Dashboard` or :class:`~horizon.Panel` classes, +the following documentation is provided. + +General information +------------------- + +Brief guides to areas of interest and importance when developing Horizon. + +.. toctree:: + :maxdepth: 1 + + contributing + testing + +Topic Guides +------------ + +Information on how to work with specific areas of Horizon can be found in +the following topic guides. + +.. toctree:: + :maxdepth: 1 + + topics/tables + topics/testing + +API Reference +------------- + +In-depth documentation for Horizon and its APIs. + +.. toctree:: + :maxdepth: 1 + + ref/run_tests + ref/horizon + ref/workflows + ref/tables + ref/tabs + ref/forms + ref/middleware + ref/context_processors + ref/decorators + ref/exceptions + ref/test + +Source Code Reference +--------------------- + +Auto-generated reference for the complete source code. + +.. toctree:: + :maxdepth: 1 + + sourcecode/autoindex + +Release Notes +============= + +.. toctree:: + :glob: + :maxdepth: 1 + + releases/* + +Information +=========== + +.. toctree:: + :maxdepth: 1 + + faq + glossary + +* :ref:`genindex` +* :ref:`modindex` diff --git a/dashboard/doc/source/intro.rst b/dashboard/doc/source/intro.rst new file mode 100644 index 00000000..a546ce99 --- /dev/null +++ b/dashboard/doc/source/intro.rst @@ -0,0 +1,124 @@ +=================== +Introducing Horizon +=================== + +.. contents:: Contents: + :local: + +Values +====== + + "Think simple" as my old master used to say - meaning reduce + the whole of its parts into the simplest terms, getting back + to first principles. + + -- Frank Lloyd Wright + +Horizon holds several key values at the core of its design and architecture: + + * Core Support: Out-of-the-box support for all core OpenStack projects. + * Extensible: Anyone can add a new component as a "first-class citizen". + * Manageable: The core codebase should be simple and easy-to-navigate. + * Consistent: Visual and interaction paradigms are maintained throughout. + * Stable: A reliable API with an emphasis on backwards-compatibility. + * Usable: Providing an *awesome* interface that people *want* to use. + +The only way to attain and uphold those ideals is to make it *easy* for +developers to implement those values. + +History +======= + +Horizon started life as a single app to manage OpenStack's compute project. +As such, all it needed was a set of views, templates, and API calls. + +From there it grew to support multiple OpenStack projects and APIs gradually, +arranged rigidly into "dash" and "syspanel" groupings. + +During the "Diablo" release cycle an initial plugin system was added using +signals to hook in additional URL patterns and add links into the "dash" +and "syspanel" navigation. + +This incremental growth served the goal of "Core Support" phenomenally, but +left "Extensible" and "Manageable" behind. And while the other key values took +shape of their own accord, it was time to re-architect for an extensible, +modular future. + + +The Current Architecture & How It Meets Our Values +================================================== + +At its core, **Horizon should be a registration pattern for +applications to hook into**. Here's what that means and how it is +implemented in terms of our values: + +Core Support +------------ + +Horizon ships with three central dashboards, a "User Dashboard", a +"System Dashboard", and a "Settings" dashboard. Between these three they +cover the core OpenStack applications and deliver on Core Support. + +The Horizon application also ships with a set of API abstractions +for the core OpenStack projects in order to provide a consistent, stable set +of reusable methods for developers. Using these abstractions, developers +working on Horizon don't need to be intimately familiar with the APIs of +each OpenStack project. + +Extensible +---------- + +A Horizon dashboard application is based around the :class:`~horizon.Dashboard` +class that provides a consistent API and set of capabilities for both +core OpenStack dashboard apps shipped with Horizon and equally for third-party +apps. The :class:`~horizon.Dashboard` class is treated as a top-level +navigation item. + +Should a developer wish to provide functionality within an existing dashboard +(e.g. adding a monitoring panel to the user dashboard) the simple registration +pattern makes it possible to write an app which hooks into other dashboards +just as easily as creating a new dashboard. All you have to do is import the +dashboard you wish to modify. + +Manageable +---------- + +Within the application, there is a simple method for registering a +:class:`~horizon.Panel` (sub-navigation items). Each panel contains the +necessary logic (views, forms, tests, etc.) for that interface. This granular +breakdown prevents files (such as ``api.py``) from becoming thousands of +lines long and makes code easy to find by correlating it directly to the +navigation. + +Consistent +---------- + +By providing the necessary core classes to build from, as well as a +solid set of reusable templates and additional tools (base form classes, +base widget classes, template tags, and perhaps even class-based views) +we can maintain consistency across applications. + +Stable +------ + +By architecting around these core classes and reusable components we +create an implicit contract that changes to these components will be +made in the most backwards-compatible ways whenever possible. + +Usable +------ + +Ultimately that's up to each and every developer that touches the code, +but if we get all the other goals out of the way then we are free to focus +on the best possible experience. + +.. seealso:: + + :doc:`Quickstart ` + A short guide to getting started with using Horizon. + + :doc:`Frequently Asked Questions ` + Common questions and answers. + + :doc:`Glossary ` + Common terms and their definitions. diff --git a/dashboard/doc/source/quickstart.rst b/dashboard/doc/source/quickstart.rst new file mode 100644 index 00000000..485328bc --- /dev/null +++ b/dashboard/doc/source/quickstart.rst @@ -0,0 +1,207 @@ +================== +Horizon Quickstart +================== + +Setup +===== + +To setup an Horizon development environment simply clone the Horizon git +repository from http://github.com/openstack/horizon and execute the +``run_tests.sh`` script from the root folder (see :doc:`ref/run_tests`):: + + > git clone https://github.com/openstack/horizon.git + > cd horizon + > ./run_tests.sh + +Next you will need to setup your Django application config by copying ``openstack_dashboard/local/local_settings.py.example`` to ``openstack_dashboard/local_settings.py``. To do this quickly you can use the following command:: + + > cp openstack_dashboard/local/local_settings.py.example openstack_dashboard/local/local_settings.py + +Horizon assumes a single end-point for OpenStack services which defaults to +the local host (127.0.0.1). If this is not the case change the +``OPENSTACK_HOST`` setting in the ``openstack_dashboard/local/local_settings.py`` file, to the actual IP address of the OpenStack end-point Horizon should use. + +To start the Horizon development server use the Django ``manage.py`` utility +with the context of the virtual environment:: + + > tools/with_venv.sh ./manage.py runserver + +Alternately specify the listen IP and port:: + + > tools/with_venv.sh ./manage.py runserver 0.0.0.0:8080 + +.. note:: + + If you would like to run commands without the prefix of ``tools/with_venv.sh`` you may source your environment directly. This will remain active as long as your shell session stays open:: + + > source .venv/bin/activate + + +Once the Horizon server is running point a web browser to http://localhost:8000 +or to the IP and port the server is listening for. + +.. note:: + + The ``DevStack`` project (http://devstack.org/) can be used to install + an OpenStack development environment from scratch. + +.. note:: + + The minimum required set of OpenStack services running includes the + following: + + * Nova (compute, api, scheduler, and network) + * Glance + * Keystone + + Optional support is provided for Swift. + +Horizon's Structure +=================== + +This project is a bit different from other OpenStack projects in that it has +two very distinct components underneath it: ``horizon``, and +``openstack_dashboard``. + +The ``horizon`` directory holds the generic libraries and components that can +be used in any Django project. + +The ``openstack_dashboard`` directory contains a reference Django project that +uses ``horizon``. + +For development, both pieces share an environment which (by default) is +built with the ``tools/install_venv.py`` script. That script creates a +virtualenv and installs all the necessary packages. + +If dependencies are added to either ``horizon`` or ``openstack_dashboard``, +they should be added to ``tools/pip-requires``. + + .. important:: + + If you do anything which changes the environment (adding new dependencies + or renaming directories are both great examples) be sure to increment the + ``environment_version`` counter in :doc:`run_tests.sh `. + +Project +======= + +INSTALLED_APPS +-------------- + +At the project level you add Horizon and any desired dashboards to your +``settings.INSTALLED_APPS``:: + + INSTALLED_APPS = ( + 'django', + ... + 'horizon', + 'horizon.dash', + 'horizon.syspanel', + ) + +URLs +---- + +Then you add a single line to your project's ``urls.py``:: + + url(r'', include(horizon.urls)), + +Those urls are automatically constructed based on the registered Horizon apps. +If a different URL structure is desired it can be constructed by hand. + +Templates +--------- + +Pre-built template tags generate navigation. In your ``nav.html`` +template you might have the following:: + + {% load horizon %} + + + +And in your ``sidebar.html`` you might have:: + + {% load horizon %} + + + +These template tags are aware of the current "active" dashboard and panel +via template context variables and will render accordingly. + +Application +=========== + +Structure +--------- + +An application would have the following structure (we'll use syspanel as +an example):: + + syspanel/ + |---__init__.py + |---dashboard.py <-----Registers the app with Horizon and sets dashboard properties + |---templates/ + |---templatetags/ + |---overview/ + |---services/ + |---images/ + |---__init__.py + |---panel.py <-----Registers the panel in the app and defines panel properties + |---urls.py + |---views.py + |---forms.py + |---tests.py + |---api.py <-------Optional additional API methods for non-core services + |---templates/ + ... + ... + +Dashboard Classes +----------------- + +Inside of ``dashboard.py`` you would have a class definition and the registration +process:: + + import horizon + + + class Syspanel(horizon.Dashboard): + name = "Syspanel" # Appears in navigation + slug = 'syspanel' # Appears in url + panels = ('overview', 'services', 'instances', 'flavors', 'images', + 'tenants', 'users', 'quotas',) + default_panel = 'overview' + permissions = ('openstack.roles.admin',) + ... + + + horizon.register(Syspanel) + +Panel Classes +------------- + +To connect a :class:`~horizon.Panel` with a :class:`~horizon.Dashboard` class +you register it in a ``panels.py`` file like so:: + + import horizon + + from horizon.dashboard.syspanel import dashboard + + + class Images(horizon.Panel): + name = "Images" + slug = 'images' + permissions = ('openstack.roles.admin', 'my.other.permission',) + + + # You could also register your panel with another application's dashboard + dashboard.Syspanel.register(Images) + +By default a :class:`~horizon.Panel` class looks for a ``urls.py`` file in the +same directory as ``panel.py`` to include in the rollup of url patterns from +panels to dashboards to Horizon, resulting in a wholly extensible, configurable +URL structure. diff --git a/dashboard/doc/source/ref/context_processors.rst b/dashboard/doc/source/ref/context_processors.rst new file mode 100644 index 00000000..b34c0109 --- /dev/null +++ b/dashboard/doc/source/ref/context_processors.rst @@ -0,0 +1,6 @@ +========================== +Horizon Context Processors +========================== + +.. automodule:: horizon.context_processors + :members: diff --git a/dashboard/doc/source/ref/decorators.rst b/dashboard/doc/source/ref/decorators.rst new file mode 100644 index 00000000..777afbe5 --- /dev/null +++ b/dashboard/doc/source/ref/decorators.rst @@ -0,0 +1,6 @@ +================== +Horizon Decorators +================== + +.. automodule:: horizon.decorators + :members: diff --git a/dashboard/doc/source/ref/exceptions.rst b/dashboard/doc/source/ref/exceptions.rst new file mode 100644 index 00000000..4151f18f --- /dev/null +++ b/dashboard/doc/source/ref/exceptions.rst @@ -0,0 +1,6 @@ +================== +Horizon Exceptions +================== + +.. automodule:: horizon.exceptions + :members: diff --git a/dashboard/doc/source/ref/forms.rst b/dashboard/doc/source/ref/forms.rst new file mode 100644 index 00000000..f0a0507c --- /dev/null +++ b/dashboard/doc/source/ref/forms.rst @@ -0,0 +1,98 @@ +============= +Horizon Forms +============= + +Horizon ships with some very useful base form classes, form fields, +class-based views, and javascript helpers which streamline most of the common +tasks related to form handling. + +Form Classes +============ + +.. automodule:: horizon.forms.base + :members: + +Form Fields +=========== + +.. automodule:: horizon.forms.fields + :members: + +Form Views +========== + +.. automodule:: horizon.forms.views + :members: + +Forms Javascript +================ + +Switchable Fields +----------------- + +By marking fields with the ``"switchable"`` and ``"switched"`` classes along +with defining a few data attributes you can programmatically hide, show, +and rename fields in a form. + +The triggers are fields using a ``select`` input widget, marked with the +"switchable" class, and defining a "data-slug" attribute. When they are changed, +any input with the ``"switched"`` class and defining a ``"data-switch-on"`` +attribute which matches the ``select`` input's ``"data-slug"`` attribute will be +evaluated for necessary changes. In simpler terms, if the ``"switched"`` target +input's ``"switch-on"`` matches the ``"slug"`` of the ``"switchable"`` trigger +input, it gets switched. Simple, right? + +The ``"switched"`` inputs also need to define states. For each state in which +the input should be shown, it should define a data attribute like the +following: ``data--=""``. When the switch event +happens the value of the ``"switchable"`` field will be compared to the +data attributes and the correct label will be applied to the field. If +a corresponding label for that value is *not* found, the field will +be hidden instead. + +A simplified example is as follows:: + + source = forms.ChoiceField( + label=_('Source'), + choices=[ + ('cidr', _('CIDR')), + ('sg', _('Security Group')) + ], + widget=forms.Select(attrs={ + 'class': 'switchable', + 'data-slug': 'source' + }) + ) + + cidr = fields.IPField( + label=_("CIDR"), + required=False, + widget=forms.TextInput(attrs={ + 'class': 'switched', + 'data-switch-on': 'source', + 'data-source-cidr': _('CIDR') + }) + ) + + security_group = forms.ChoiceField( + label=_('Security Group'), + required=False, + widget=forms.Select(attrs={ + 'class': 'switched', + 'data-switch-on': 'source', + 'data-source-sg': _('Security Group') + }) + ) + +That code would create the ``"switchable"`` control field ``source``, and the +two ``"switched"`` fields ``cidr`` and ``security group`` which are hidden or +shown depending on the value of ``source``. + + +NOTE: A field can only safely define one slug in its ``"switch-on"`` attribute. +While switching on multiple fields is possible, the behavior is very hard to +predict due to the events being fired from the various switchable fields in +order. You generally end up just having it hidden most of the time by accident, +so it's not recommended. Instead just add a second field to the form and control +the two independently, then merge their results in the form's clean or handle +methods at the end. diff --git a/dashboard/doc/source/ref/horizon.rst b/dashboard/doc/source/ref/horizon.rst new file mode 100644 index 00000000..cc4b2d70 --- /dev/null +++ b/dashboard/doc/source/ref/horizon.rst @@ -0,0 +1,45 @@ +====================== +The ``horizon`` Module +====================== + +.. module:: horizon + +Horizon ships with a single point of contact for hooking into your project if +you aren't developing your own :class:`~horizon.Dashboard` or +:class:`~horizon.Panel`:: + + import horizon + +From there you can access all the key methods you need. + +Horizon +======= + +.. attribute:: urls + + The auto-generated URLconf for Horizon. Usage:: + + url(r'', include(horizon.urls)), + +.. autofunction:: register +.. autofunction:: unregister +.. autofunction:: get_absolute_url +.. autofunction:: get_user_home +.. autofunction:: get_dashboard +.. autofunction:: get_default_dashboard +.. autofunction:: get_dashboards + +Dashboard +========= + +.. autoclass:: Dashboard + :members: + +Panel +===== + +.. autoclass:: Panel + :members: + +.. autoclass:: PanelGroup + :members: diff --git a/dashboard/doc/source/ref/middleware.rst b/dashboard/doc/source/ref/middleware.rst new file mode 100644 index 00000000..fcca5ff0 --- /dev/null +++ b/dashboard/doc/source/ref/middleware.rst @@ -0,0 +1,6 @@ +================== +Horizon Middleware +================== + +.. automodule:: horizon.middleware + :members: diff --git a/dashboard/doc/source/ref/run_tests.rst b/dashboard/doc/source/ref/run_tests.rst new file mode 100644 index 00000000..5bcc1d75 --- /dev/null +++ b/dashboard/doc/source/ref/run_tests.rst @@ -0,0 +1,224 @@ +=========================== +The ``run_tests.sh`` Script +=========================== + +.. contents:: Contents: + :local: + +Horizon ships with a script called ``run_tests.sh`` at the root of the +repository. This script provides many crucial functions for the project, +and also makes several otherwise complex tasks trivial for you as a +developer. + +First Run +========= + +If you start with a clean copy of the Horizon repository, the first thing +you should do is to run ``./run_tests.sh`` from the root of the repository. +This will do two things for you: + + #. Set up a virtual environment for both the ``horizon`` module and + the ``openstack-dashboard`` project using + ``openstack-dashboard/tools/install_venv.py``. + #. Run the tests for both ``horizon`` and ``openstack-dashboard`` using + their respective environments and verify that evreything is working. + +Setting up the environment the first time can take several minutes, but only +needs to be done once. If dependencies are added in the future, updating the +environments will be necessary but not as time consuming. + +I just want to run the tests! +============================= + +Running the full set of unit tests quickly and easily is the main goal of this +script. All you need to do is:: + + ./run_tests.sh + +Yep, that's it. However, for a quicker test run you can skip the Selenium +tests by using the ``--skip-selenium`` flag:: + + ./run_tests.sh --skip-selenium + +This isn't recommended, but can be a timesaver when you only need to run +the code tests and not the frontend tests during development. + +Running a subset of tests +------------------------- + +Instead of running all tests, you can specify an individual directory, file, +class, or method that contains test code. + +To run the tests in the ``horizon/test/tests/tables.py`` file:: + + ./run_tests.sh horizon.test.tests.tables + +To run the tests in the `WorkflowsTests` class in +``horizon/test/tests/workflows``:: + + ./run_tests.sh horizon.test.tests.workflows:WorkflowsTests + +To run just the `WorkflowsTests.test_workflow_view` test method:: + + ./run_tests.sh horizon.test.tests.workflows:WorkflowsTests.test_workflow_view + +Using Dashboard and Panel Templates +=================================== + +Horizon has a set of convenient management commands for creating new +dashboards and panels based on basic templates. + +Dashboards +---------- + +To create a new dashboard, run the following: + + ./run_tests.sh -m startdash + +This will create a directory with the given dashboard name, a ``dashboard.py`` +module with the basic dashboard code filled in, and various other common +"boilerplate" code. + +Available options: + +* --target: the directory in which the dashboard files should be created. + Default: A new directory within the current directory. + +Panels +------ + +To create a new panel, run the following: + + ./run_tests -m startpanel --dashboard= + +This will create a directory with the given panel name, and ``panel.py`` +module with the basic panel code filled in, and various other common +"boilerplate" code. + +Available options: + +* -d, --dashboard: The dotted python path to your dashboard app (the module + which containers the ``dashboard.py`` file.). +* --target: the directory in which the panel files should be created. + If the value is ``auto`` the panel will be created as a new directory inside + the dashboard module's directory structure. Default: A new directory within + the current directory. + +Give me metrics! +================ + +You can generate various reports and metrics using command line arguments +to ``run_tests.sh``. + +Coverage +-------- + +To run coverage reports:: + + ./run_tests.sh --coverage + +The reports are saved to ``./reports/`` and ``./coverage.xml``. + +PEP8 +---- + +You can check for PEP8 violations as well:: + + ./run_tests.sh --pep8 + +The results are saved to ``./pep8.txt``. + +PyLint +------ + +For more detailed code analysis you can run:: + + ./run_tests.sh --pylint + +The output will be saved in ``./pylint.txt``. + +Tab Characters +-------------- + +For those who dislike having a mix of tab characters and spaces for indentation +there's a command to check for that in Python, CSS, JavaScript and HTML files:: + + ./run_tests.sh --tabs + +This will output a total "tab count" and a list of the offending files. + +Running the development server +============================== + +As an added bonus, you can run Django's development server directly from +the root of the repository with ``run_tests.sh`` like so:: + + ./run_tests.sh --runserver + +This is effectively just an alias for:: + + ./openstack-dashboard/tools/with_venv.sh ./openstack-dashboard/dashboard/manage.py runserver + +Generating the documentation +============================ + +You can build Horizon's documentation automatically by running:: + + ./run_tests.sh --docs + +The output is stored in ``./doc/build/html/``. + +Updating the translation files +============================== + +You can update all of the translation files for both the ``horizon`` app and +``openstack_dashboard`` project with a single command: + + ./run_tests.sh --makemessages + +or, more compactly: + + ./run_tests.sh --m + +Starting clean +============== + +If you ever want to start clean with a new environment for Horizon, you can +run:: + + ./run_tests.sh --force + +That will blow away the existing environments and create new ones for you. + +Non-interactive Mode +==================== + +There is an optional flag which will run the script in a non-interactive +(and eventually less verbose) mode:: + + ./run_tests.sh --quiet + +This will automatically take the default action for actions which would +normally prompt for user input such as installing/updating the environment. + +Environment Backups +=================== + +To speed up the process of doing clean checkouts, running continuous +integration tests, etc. there are options for backing up the current +environment and restoring from a backup. + + ./run_tests.sh --restore-environment + ./run_tests.sh --backup-environment + +The environment backup is stored in ``/tmp/.horizon_environment/``. + +Environment Versioning +====================== + +Horizon keeps track of changes to the environment by incrementing an +``environment_version`` integer at the top of ``run_tests.sh``. + +If you do anything which changes the environment (adding new dependencies +or renaming directories are both great examples) be sure to increment the +``environment_version`` counter as well. diff --git a/dashboard/doc/source/ref/tables.rst b/dashboard/doc/source/ref/tables.rst new file mode 100644 index 00000000..4b74d5e3 --- /dev/null +++ b/dashboard/doc/source/ref/tables.rst @@ -0,0 +1,82 @@ +================== +Horizon DataTables +================== + +.. module:: horizon.tables + +Horizon includes a componentized API for programmatically creating tables +in the UI. Why would you want this? It means that every table renders +correctly and consistently, table- and row-level actions all have a consistent +API and appearance, and generally you don't have to reinvent the wheel or +copy-and-paste every time you need a new table! + +DataTable +========= + +The core class which defines the high-level structure of the table being +represented. Example:: + + class MyTable(DataTable): + name = Column('name') + email = Column('email') + + class Meta: + name = "my_table" + table_actions = (MyAction, MyOtherAction) + row_actions - (MyAction) + +A full reference is included below: + +.. autoclass:: DataTable + :members: + +DataTable Options +================= + +The following options can be defined in a ``Meta`` class inside a +:class:`.DataTable` class. Example:: + + class MyTable(DataTable): + class Meta: + name = "my_table" + verbose_name = "My Table" + +.. autoclass:: horizon.tables.base.DataTableOptions + :members: + +Table Components +================ + +.. autoclass:: Column + :members: + +.. autoclass:: Row + :members: + +Actions +======= + +.. autoclass:: Action + :members: + +.. autoclass:: LinkAction + :members: + +.. autoclass:: FilterAction + :members: + +.. autoclass:: BatchAction + :members: + +.. autoclass:: DeleteAction + :members: + +Class-Based Views +================= + +Several class-based views are provided to make working with DataTables +easier in your UI. + +.. autoclass:: DataTableView + +.. autoclass:: MultiTableView diff --git a/dashboard/doc/source/ref/tabs.rst b/dashboard/doc/source/ref/tabs.rst new file mode 100644 index 00000000..807385ca --- /dev/null +++ b/dashboard/doc/source/ref/tabs.rst @@ -0,0 +1,45 @@ +========================== +Horizon Tabs and TabGroups +========================== + +.. module:: horizon.tabs + +Horizon includes a set of reusable components for programmatically +building tabbed interfaces with fancy features like dynamic AJAX loading +and nearly effortless templating and styling. + +Tab Groups +========== + +For any tabbed interface, your fundamental element is the tab group which +contains all your tabs. This class provides a dead-simple API for building +tab groups and encapsulates all the necessary logic behind the scenes. + +.. autoclass:: TabGroup + :members: + +Tabs +==== + +The tab itself is the discrete unit for a tab group, representing one +view of data. + +.. autoclass:: Tab + :members: + +.. autoclass:: TableTab + :members: + + + +TabView +======= + +There is also a useful and simple generic class-based view for handling +the display of a :class:`~horizon.tabs.TabGroup` class. + +.. autoclass:: TabView + :members: + +.. autoclass:: TabbedTableView + :members: diff --git a/dashboard/doc/source/ref/test.rst b/dashboard/doc/source/ref/test.rst new file mode 100644 index 00000000..79a4bc1c --- /dev/null +++ b/dashboard/doc/source/ref/test.rst @@ -0,0 +1,25 @@ +======================== +Horizon TestCase Classes +======================== + +.. module:: horizon.test.helpers + +Horizon provides a base test case class which provides several useful +pre-prepared attributes for testing Horizon components. + +.. autoclass:: TestCase + :members: + +.. module :: openstack_dashboard.test.helpers + +The OpenStack Dashboard also provides test case classes for greater +ease-of-use when testing APIs and OpenStack-specific auth scenarios. + +.. autoclass:: TestCase + :members: + +.. autoclass:: APITestCase + :members: + +.. autoclass:: BaseAdminViewTests + :members: diff --git a/dashboard/doc/source/ref/workflows.rst b/dashboard/doc/source/ref/workflows.rst new file mode 100644 index 00000000..c4077667 --- /dev/null +++ b/dashboard/doc/source/ref/workflows.rst @@ -0,0 +1,33 @@ +================= +Horizon Workflows +================= + +.. module:: horizon.workflows + +One of the most challenging aspects of building a compelling user experience +is crafting complex multi-part workflows. Horizon's ``workflows`` module +aims to bring that capability within everyday reach. + +Workflows +========= + +.. autoclass:: Workflow + :members: + +Steps +===== + +.. autoclass:: Step + :members: + +Actions +======= + +.. autoclass:: Action + :members: + +WorkflowView +============ + +.. autoclass:: WorkflowView + :members: diff --git a/dashboard/doc/source/releases/2012_1.rst b/dashboard/doc/source/releases/2012_1.rst new file mode 100644 index 00000000..e024bf41 --- /dev/null +++ b/dashboard/doc/source/releases/2012_1.rst @@ -0,0 +1,148 @@ +====================== +Horizon 2012.1 "Essex" +====================== + +Release Overview +================ + +During the Essex release cycle, Horizon underwent a significant set of internal +changes to allow extensibility and customization while also adding a significant +number of new features and bringing much greater stability to every interaction +with the underlying components. + +Highlights +========== + +Extensibility +------------- + +Making Horizon extensible for third-party developers was one of the core +goals for the Essex release cycle. Massive strides have been made to allow +for the addition of new "plug-in" components and customization of OpenStack +Dashboard deployments. + +To support this extensability, all the components used to build on Horizon's +interface are now modular and reusable. Horizon's own dashboards use these +components, and they have all been built with third-party developers in mind. +Some of the main components are listed below. + +Dashboards and Panels +~~~~~~~~~~~~~~~~~~~~~ + +Horizon's structure has been divided into logical groupings called dashboards +and panels. Horizon's classes representing these concepts handle all the +structural concerns associated with building a complete user interface +(navigation, access control, url structure, etc.). + +Data Tables +~~~~~~~~~~~ + +One of the most common activities in a dashboard user interface is simply +displaying a list of resources or data and allowing the user to take actions on +that data. To this end, Horizon abstracted the commonalities of this task into a +reusable set of classes which allow developers to programmatically create +displays and interactions for their data with minimal effort and zero +boilerplate. + +Tabs and TabGroups +~~~~~~~~~~~~~~~~~~ + +Another extremely common user-interface element is the use of "tabs" to break +down discrete groups of data into manageable chunks. Since these tabs often +encompasse vastly different data, may have completely different access +restrictions, and may sometimes be better-off being loaded dynamically rather +than with the initial page load, Horizon includes tab and tab group classes for +constructing these interfaces elegently and with no knowledge of the HTML, CSS +or JavaScript involved. + +Nova Features +------------- + +Support for Nova's features has been greatly improved in Essex: + +* Support for Nova volumes, including: + * Volumes creation and management. + * Volume snapshots. + * Realtime AJAX updating for volumes in transition states. +* Improved Nova instance display and interactions, including: + * Launching instances from volumes. + * Pausing/suspending instances. + * Displaying instance power states. + * Realtime AJAX updating for instances in transition states. +* Support for managing Floating IP address pools. +* New instance and volume detail views. + +Settings +-------- + +A new "Settings" area was added that offers several userful functions: + +* EC2 credentials download. +* OpenStack RC file download. +* User language preference customization. + +User Experience Improvements +---------------------------- + +* Support for batch actions on multiple resources (e.g. terminating multiple + instances at once). +* Modal interactions throughout the entire UI. +* AJAX form submission for in-place validation. +* Improved in-context help for forms (tooltips and validation messages). + + +Community +--------- + +* Creation and publication of a set of Human Interface Guidelines (HIG). +* Copious amounts of documentation for developers. + +Under The Hood +-------------- + +* Internationalization fully enabled, with all strings marked for translation. +* Client library changes: + * Full migration to python-novaclient from the deprecated openstackx library. + * Migration to python-keystoneclient from the deprecated keystone portion + of the python-novaclient library. +* Client-side templating capabilities for more easily creating dynamic + interactions. +* Frontend overhaul to use the Bootstrap CSS/JS framework. +* Centralized error handling for vastly improved stability/reliability + across APIs/clients. +* Completely revamped test suite with comprehensive test data. +* Forward-compatibility with Django 1.4 and the option of cookie-based sessions. + +Known Issues and Limitations +============================ + +Quantum +------- + +Quantum support has been removed from Horizon for the Essex release. It will be +restored in Folsom in conjunction with Quantum's first release as a core +OpenStack project. + +Keystone +-------- + +Due to the mechanisms by which Keystone determines "admin"-ness for a user, an +admin user interacting with the "Project" dashboard may see some inconsistent +behavior such as all resources being listed instead of only those belonging to +that project, or only being able to return to the "Admin" dashboard while +accessing certain projects. + +Exceptions during customization +------------------------------- + +Exceptions raised while overriding built-in Horizon behavior via the +"customization_module" setting may trigger a bug in the error handling +which will mask the original exception. + +Backwards Compatibility +======================= + +The Essex Horizon release is only partially backwards-compatible with Diablo +OpenStack components. While it is largely possible to log in and interact, many +functions in Nova, Glance and Keystone changed too substantially in Essex to +maintain full compatibliity. diff --git a/dashboard/doc/source/releases/2012_2.rst b/dashboard/doc/source/releases/2012_2.rst new file mode 100644 index 00000000..0ff3e5c8 --- /dev/null +++ b/dashboard/doc/source/releases/2012_2.rst @@ -0,0 +1,159 @@ +======================= +Horizon 2012.2 "Folsom" +======================= + +Release Overview +================ + +The Folsom release cycle brought several major advances to Horizon's user +experience while also reintroducing Quantum networking as a core piece +of the OpenStack Dashboard. + +Highlights +========== + +Networking (Quantum) +-------------------- + +With Quantum being a core project for the Folsom release, we worked closely +with the Quantum team to bring networking support back into Horizon. This +appears in two primary places: the Networks panel in both the Project and +Admin dashboards, and the Network tab in the Launch Instance workflow. Expect +further improvements in these areas as Quantum continues to mature and more +users adopt this model of virtual network management. + +User Experience +--------------- + +Workflows +~~~~~~~~~ + +By far the biggest UI/UX change in the Folsom release is the introduction of +programmatic workflows. These components allow developers to create concise +interactions that combine discrete tasks spanning multiple services and +resources in a user-friendly way and with minimal boilerplate code. Within +a workflow, related objects can also be dynamically created so users don't lose +their place when they realize the item they wanted isn't currently available. +Look for examples of these workflows in Launch Instance, Associate Floating IP, +and Create/Edit Project. + +Resource Browser +~~~~~~~~~~~~~~~~ + +Another cool new component is an interface designed for "browsing" resources +which are nested under a parent resource. The object store (Swift) is a prime +example of this. Now there is a consistent top-level navigation for containers +on the left-hand pane of the "browser" while the right-hand pane lets you +explore within those containers and sub-folders. + +User Experience Improvements +---------------------------- + +* Timezone support is now enabled. You can select your preferred timezone + in the User Settings panel. + +Community +--------- + +* Third-party developers who wish to build on Horizon can get started much + faster using the new dashboard and panel templates. See the docs on + `creating a dashboard`_ and `creating a panel`_ for more information. + +* A `thorough set of documentation`_ for developers on how to go about + internationalizing, localizing and translating OpenStack projects + is now available. + +.. _creating a dashboard: http://docs.openstack.org/developer/horizon/topics/tutorial.html#creating-a-dashboard +.. _creating a panel: http://docs.openstack.org/developer/horizon/topics/tutorial.html#creating-a-panel +.. _thorough set of documentation: http://wiki.openstack.org/Translations + +Under The Hood +-------------- + +* The python-swiftclient library and python-cinderclient libraries are now + used under the hood instead of cloudfiles and python-novaclient respectively. + +* Internationalization of client-side JavaScript is now possible in addition + to server-side Python code. + +* Keystone authentication is now handled by a proper pluggable Django + authentication backend, offering significantly better and more reliable + security for Horizon. + +Other Improvements and Fixes +---------------------------- + +Some of the general areas of improvement include: + +* Images can now be added to Glance by providing a URL for Glance to download + the image data from. + +* Quotas are now displayed dynamically throughout the Project dashboard. + +* API endpoints are now displayed on the OpenStack RC File panel so they + can be organically discovered by an end-user. + +* DataTables now support a summation row at the bottom of the table. + +* Better cross-browser support (Safari and IE particularly). + +* Fewer API calls to OpenStack endpoints (improves performance). + +* Better validation of what actions are permitted when. + +* Improved error handling and error messages. + +Known Issues and Limitations +============================ + +Floating IPs and Quantum +------------------------ + +Due to the very late addition of floating IP support in Quantum, Nova's +integration there is lacking, so floating IP-related API calls to Nova will +fail when your OpenStack deployment uses Quantum for networking. This means +that Horizon actions such as "allocate" and "associate" floating IPs will +not work either since they rely on the underlying APIs. + +Pagination +---------- + +A number of the "index" pages don't fully work with API pagination yet, +causing them to only display the first chunk of results returned by the API. +This number is often 1000 (as in the case of novaclient results), but does vary +somewhat. + +Deleting large numbers of resources simultaneously +-------------------------------------------------- + +Using the "select all" checkbox to delete large numbers of resources via the +API can cause network timeouts (depending on configuration). This is +due to the APIs not supporting bulk-deletion natively, and consequently Horizon +has to send requests to delete each resource individually behind the scenes. + +Backwards Compatibility +======================= + +The Folsom Horizon release should be fully-compatible with both Folsom and +Essex versions of the rest of the OpenStack core projects (Nova, Swift, etc.). +While some features work significantly better with an all-Folsom stack due +to bugfixes, etc. in underlying services, there should not be any limitations +on what will or will not function. (Note: Quantum was not a core OpenStack +project in Essex, and thus this statement does not apply to network management.) + +In terms of APIs provided for extending Horizon, there are a handful of +backwards-incompatible changes that were made: + +* The ``can_haz`` and ``can_haz_list`` template filters have been renamed + to ``has_permissions`` and ``has_permissions_on_list`` respectively. + +* The dashboard-specific ``base.html`` templates (e.g. ``nova/base.html``, + ``syspanel/base.html``, etc.) have been removed in favor of a single + ``base.html`` template. + +* In conjunction with the previous item, the dashboard-specific template blocks + (e.g. ``nova_main``, ``syspanel_main``, etc.) have been removed in favor of + a single ``main`` template block. + +Overall, though, great effort has been made to maintain compatibility for +third-party developers who may have built on Horizon so far. \ No newline at end of file diff --git a/dashboard/doc/source/testing.rst b/dashboard/doc/source/testing.rst new file mode 100644 index 00000000..d9362327 --- /dev/null +++ b/dashboard/doc/source/testing.rst @@ -0,0 +1,41 @@ +======================= +Horizon's tests and you +======================= + +How to run the tests +==================== + +Because Horizon is composed of both the ``horizon`` app and the +``openstack-dashboard`` reference project, there are in fact two sets of unit +tests. While they can be run individually without problem, there is an easier +way: + +Included at the root of the repository is the ``run_tests.sh`` script +which invokes both sets of tests, and optionally generates analyses on both +components in the process. This script is what what Jenkins uses to verify the +stability of the project, so you should make sure you run it and it passes +before you submit any pull requests/patches. + +To run the tests:: + + $ ./run_tests.sh + +It's also possible to :doc:`run a subset of unit tests`. + +.. seealso:: + + :doc:`ref/run_tests` + Full reference for the ``run_tests.sh`` script. + +Writing tests +============= + +Horizon uses Django's unit test machinery (which extends Python's ``unittest2`` +library) as the core of its test suite. As such, all tests for the Python code +should be written as unit tests. No doctests please. + +In general new code without unit tests will not be accepted, and every bugfix +*must* include a regression test. + +For a much more in-depth discussion of testing, see the :doc:`testing topic +guide `. diff --git a/dashboard/doc/source/topics/customizing.rst b/dashboard/doc/source/topics/customizing.rst new file mode 100644 index 00000000..5566ae19 --- /dev/null +++ b/dashboard/doc/source/topics/customizing.rst @@ -0,0 +1,139 @@ +=================== +Customizing Horizon +=================== + +Changing the Site Title +======================= + +The OpenStack Dashboard Site Title branding (i.e. "**OpenStack** Dashboard") +can be overwritten by adding the attribute ``SITE_BRANDING`` +to ``local_settings.py`` with the value being the desired name. + +The file ``local_settings.py`` can be found at the Horizon directory path of +``horizon/openstack-dashboard/local/local_settings.py``. + +Changing the Logo +================= + +The OpenStack Logo is pulled in through ``style.css``:: + + #splash .modal { + background: #fff url(../images/logo.png) no-repeat center 35px; + + h1.brand a { + background: url(../images/logo.png) top left no-repeat; + +To override the OpenStack Logo image, replace the image at the directory path +``horizon/openstack-dashboard/dashboard/static/dashboard/images/logo.png``. + +The dimensions should be ``width: 108px, height: 121px``. + +Modifying Existing Dashboards and Panels +======================================== + +If you wish to alter dashboards or panels which are not part of your codebase, +you can specify a custom python module which will be loaded after the entire +Horizon site has been initialized, but prior to the URLconf construction. +This allows for common site-customization requirements such as: + +* Registering or unregistering panels from an existing dashboard. +* Changing the names of dashboards and panels. +* Re-ordering panels within a dashboard or panel group. + +To specify the python module containing your modifications, add the key +``customization_module`` to your ``settings.HORIZON_CONFIG`` dictionary. +The value should be a string containing the path to your module in dotted +python path notation. Example:: + + HORIZON_CONFIG = { + "customization_module": "my_project.overrides" + } + +You can do essentially anything you like in the customization module. For +example, you could change the name of a panel:: + + from django.utils.translation import ugettext_lazy as _ + + import horizon + + # Rename "User Settings" to "User Options" + settings = horizon.get_dashboard("settings") + user_panel = settings.get_panel("user") + user_panel.name = _("User Options") + +Or get the instances panel:: + + projects_dashboard = horizon.get_dashboard("project") + instances_panel = projects_dashboard.get_panel("instances") + +And limit access to users with the Keystone Admin role:: + + permissions = list(getattr(instances_panel, 'permissions', [])) + permissions.append('openstack.roles.admin') + instances_panel.permissions = tuple(permissions) + +Or just remove it entirely:: + + projects_dashboard.unregister(instances_panel.__class__) + + + +.. NOTE:: + + ``my_project.overrides`` needs to be importable by the python process running + Horizon. + If your module is not installed as a system-wide python package, + you can either make it installable (e.g., with a setup.py) + or you can adjust the python path used by your WSGI server to include its location. + + Probably the easiest way is to add a ``python-path`` argument to + the ``WSGIDaemonProcess`` line in Apache's Horizon config. + + Assuming your ``my_project`` module lives in ``/opt/python/my_project``, + you'd make it look like the following:: + + WSGIDaemonProcess [... existing options ...] python-path=/opt/python + + +Button Icons +============ + +Horizon provides hooks for customizing the look and feel of each class of +button on the site. The following classes are used to identify each type of +button: + +* Generic Classes + * btn-search + * btn-delete + * btn-upload + * btn-download + * btn-create + * btn-edit + * btn-list + * btn-copy + * btn-camera + * btn-stats + * btn-enable + * btn-disable + +* Floating IP-specific Classes + * btn-allocate + * btn-release + * btn-associate + * btn-disassociate + +* Instance-specific Classes + * btn-launch + * btn-terminate + * btn-reboot + * btn-pause + * btn-suspend + * btn-console + * btn-log + +* Volume-specific classes + * btn-detach + +Additionally, the site-wide default button classes can be configured by +setting ``ACTION_CSS_CLASSES`` to a tuple of the classes you wish to appear +on all action buttons in your ``local_settings.py`` file. diff --git a/dashboard/doc/source/topics/deployment.rst b/dashboard/doc/source/topics/deployment.rst new file mode 100644 index 00000000..872b96d3 --- /dev/null +++ b/dashboard/doc/source/topics/deployment.rst @@ -0,0 +1,190 @@ +================= +Deploying Horizon +================= + +This guide aims to cover some common questions, concerns and pitfalls you +may encounter when deploying Horizon in a production environment. + +Logging +======= + +Logging is an important concern for prouction deployments, and the intricacies +of good logging configuration go far beyond what can be covered here. However +there are a few points worth noting about the logging included with Horizon, +how to customize it, and where other components may take over: + +* Horizon's logging uses Django's logging configuration mechanism, which + can be customized in your ``local_settings.py`` file through the + ``LOGGING`` dictionary. +* Horizon's default logging example sets the log level to ``"INFO"``, which is + a reasonable choice for production deployments. For development, however, + you may want to change the log level to ``"DEBUG"``. +* Horizon also uses a number of 3rd-party clients which log separately. The + log level for these can still be controlled through Horizon's ``LOGGING`` + config, however behaviors may vary beyond Horizon's control. + +.. warning:: + + At this time there is `a known bug in python-keystoneclient`_ where it will + log the complete request body of any request sent to Keystone through it + (including logging passwords in plain text) when the log level is set to + ``"DEBUG"``. If this behavior is not desired, make sure your log level is + ``"INFO"`` or higher. + +.. _a known bug in python-keystoneclient: https://bugs.launchpad.net/keystone/+bug/1004114 + +File Uploads +============ + +Horizon allows users to upload files via their web browser to other OpenStack +services such as Glance and Swift. Files uploaded through this mechanism are +first stored on the Horizon server before being forwarded on - files are not +uploaded directly or streamed as Horizon receives them. As Horizon itself does +not impose any restrictions on the size of file uploads, production deployments +will want to consider configuring their server hosting the Horizon application +to enforce such a limit to prevent large uploads exhausting system resources +and disrupting services. Deployments using Apache2 can use the +`LimitRequestBody directive`_ to achieve this. + +Uploads to the Glance image store service tend to be particularly large - in +the order of hundreds of megabytes to multiple gigabytes. Deployments are able +to disable the ability to upload images through Horizon by setting +``HORIZON_IMAGES_ALLOW_UPLOAD`` to ``False`` in your ``local_settings.py`` +file. + + .. _LimitRequestBody directive: http://httpd.apache.org/docs/2.2/mod/core.html#limitrequestbody + +Session Storage +=============== + +Horizon uses `Django's sessions framework`_ for handling user session data; +however that's not the end of the story. There are numerous session backends +available, which are controlled through the ``SESSION_ENGINE`` setting in +your ``local_settings.py`` file. What follows is a quick discussion of the +pros and cons of each of the common options as they pertain to deploying +Horizon specifically. + +.. _Django's sessions framework: https://docs.djangoproject.com/en/dev/topics/http/sessions/ + +Local Memory Cache +------------------ + +Enabled by:: + + SESSION_ENGINE = 'django.contrib.sessions.backends.cache' + CACHES = { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache' + } + +Local memory storage is the quickest and easiest session backend to set up, +as it has no external dependencies whatsoever. However, it has two significant +drawbacks: + + * No shared storage across processes or workers. + * No persistence after a process terminates. + +The local memory backend is enabled as the default for Horizon solely because +it has no dependencies. It is not recommended for production use, or even for +serious development work. For better options, read on. + +Memcached +--------- + +Enabled by:: + + SESSION_ENGINE = 'django.contrib.sessions.backends.cache' + CACHES = { + 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache' + 'LOCATION': 'my_memcached_host:11211', + } + +External caching using an application such as memcached offers persistence +and shared storage, and can be very useful for small-scale deployment and/or +development. However, for distributed and high-availability scenarios +memcached has inherent problems which are beyond the scope of this +documentation. + +Memcached is an extremely fast and efficient cache backend for cases where it +fits the depooyment need. But it's not appropriate for all scenarios. + +Requirements: + + * Memcached service running and accessible. + * Python memcached module installed. + +Database +-------- + +Enabled by:: + + SESSION_ENGINE = 'django.core.cache.backends.db.DatabaseCache' + DATABASES = { + 'default': { + # Databe configuration here + } + } + +Database-backed sessions are scalable (using an appropriate database strategy), +persistent, and can be made high-concurrency and highly-available. + +The downside to this approach is that database-backed sessions are one of the +slower session storages, and incur a high overhead under heavy usage. Proper +configuration of your database deployment can also be a substantial +undertaking and is far beyond the scope of this documentation. + +Cached Database +--------------- + +To mitigate the performance issues of database queries, you can also consider +using Django's ``cached_db`` session backend which utilizes both your database +and caching infrastructure to perform write-through caching and efficient +retrieval. You can enable this hybrid setting by configuring both your database +and cache as discussed above and then using:: + + SESSION_ENGINE = "django.contrib.sessions.backends.cached_db" + +Cookies +------- + +If you're using Django 1.4 or later, a new session backend is available to you +which avoids server load and scaling problems: the ``signed_cookies`` backend! + +This backend stores session data in a cookie which is stored by the +user's browser. The backend uses a cryptographic signing technique to ensure +session data is not tampered with during transport (**this is not the same +as encryption, session data is still readable by an attacker**). + +The pros of this session engine are that it doesn't require any additional +dependencies or infrastructure overhead, and it scales indefinitely as long +as the quantity of session data being stored fits into a normal cookie. + +The biggest downside is that it places session data into storage on the user's +machine and transports it over the wire. It also limits the quantity of +session data which can be stored. + +For a thorough discussion of the security implications of this session backend, +please read the `Django documentation on cookie-based sessions`_. + +.. _Django documentation on cookie-based sessions: https://docs.djangoproject.com/en/dev/topics/http/sessions/#using-cookie-based-sessions + +Secure Site Recommendations +--------------------------- + +When implementing Horizon for public usage, with the website served through +HTTPS, it is recommended that the following settings are applied. + +To help protect the session cookies from `cross-site scripting`_, add the +following to ``local_settings.py`` : + + CSRF_COOKIE_SECURE = True + SESSION_COOKIE_SECURE = True + SESSION_COOKIE_HTTPONLY = True + +Note that the CSRF_COOKIE_SECURE option is only available from Django 1.4. It +does no harm to have the setting in earlier versions, but it does not take effect. + +You can also disable `browser autocompletion`_ for the authentication form by +changing the ``password_autocomplete`` attribute to ``off`` in ``horizon/conf/default.py`` + +.. _cross-site scripting: https://www.owasp.org/index.php/HttpOnly +.. _browser autocompletion: https://wiki.mozilla.org/The_autocomplete_attribute_and_web_documents_using_XHTML diff --git a/dashboard/doc/source/topics/tables.rst b/dashboard/doc/source/topics/tables.rst new file mode 100644 index 00000000..b59dffb2 --- /dev/null +++ b/dashboard/doc/source/topics/tables.rst @@ -0,0 +1,129 @@ +====================== +DataTables Topic Guide +====================== + +Horizon provides the :mod:`horizon.tables` module to provide +a convenient, reusable API for building data-driven displays and interfaces. +The core components of this API fall into three categories: ``DataTables``, +``Actions``, and ``Class-based Views``. + + .. seealso:: + + For a detailed API information check out the :doc:`DataTables Reference + Guide `. + +Tables +====== + +The majority of interface in a dashboard-style interface ends up being +tabular displays of the various resources the dashboard interacts with. +The :class:`~horizon.tables.DataTable` class exists so you don't have to +reinvent the wheel each time. + +Creating your own tables +------------------------ + +Creating a table is fairly simple: + + #. Create a subclass of :class:`~horizon.tables.DataTable`. + #. Define columns on it using :class:`~horizon.tables.Column`. + #. Create an inner ``Meta`` class to contain the special options for + this table. + #. Define any actions for the table, and add them to + :attr:`~horizon.tables.DataTableOptions.table_actions` or + :attr:`~horizon.tables.DataTableOptions.row_actions`. + +Examples of this can be found in any of the ``tables.py`` modules included +in the reference modules under ``horizon.dashboards``. + +Connecting a table to a view +---------------------------- + +Once you've got your table set up the way you like it, the next step is to +wire it up to a view. To make this as easy as possible Horizon provides the +:class:`~horizon.tables.DataTableView` class-based view which can be subclassed +to display your table with just a couple lines of code. At it's simplest it +looks like this:: + + from horizon import tables + from .tables import MyTable + + + class MyTableView(tables.DataTableView): + table_class = MyTable + template_name = "my_app/my_table_view.html" + + def get_data(self): + return my_api.objects.list() + +In the template you would just need to include the following to render the +table:: + + {{ table.render }} + +That's it! Easy, right? + +Actions +======= + +Actions comprise any manipulations that might happen on the data in the table +or the table itself. For example, this may be the standard object CRUD, linking +to related views based on the object's id, filtering the data in the table, +or fetching updated data when appropriate. + +When actions get run +-------------------- + +There are two points in the request-response cycle in which actions can +take place; prior to data being loaded into the table, and after the data +is loaded. When you're using one of the pre-built class-based views for +working with your tables the pseudo-workflow looks like this: + + #. The request enters view. + #. The table class is instantiated without data. + #. Any "preemptive" actions are checked to see if they should run. + #. Data is fetched and loaded into the table. + #. All other actions are checked to see if they should run. + #. If none of the actions have caused an early exit from the view, + the standard response from the view is returned (usually the + rendered table). + +The benefit of the multi-step table instantiation is that you can use +preemptive actions which don't need access to the entire collection of data +to save yourself on processing overhead, API calls, etc. + +Basic actions +------------- + +At their simplest, there are three types of actions: actions which act on the +data in the table, actions which link to related resources, and actions that +alter which data is displayed. These correspond to +:class:`~horizon.tables.Action`, :class:`~horizon.tables.LinkAction`, and +:class:`~horizon.tables.FilterAction`. + +Writing your own actions generally starts with subclassing one of those +action classes and customizing the designated attributes and methods. + +Shortcut actions +---------------- + +There are several common tasks for which Horizon provides pre-built shortcut +classes. These include :class:`~horizon.tables.BatchAction`, and +:class:`~horizon.tables.DeleteAction`. Each of these abstracts away nearly +all of the boilerplate associated with writing these types of actions and +provides consistent error handling, logging, and user-facing interaction. + +It is worth noting that ``BatchAction`` and ``DeleteAction`` are extensions +of the standard ``Action`` class. + +Preemptive actions +------------------ + +Action classes which have their :attr:`~horizon.tables.Action.preempt` +attribute set to ``True`` will be evaluated before any data is loaded into +the table. As such, you must be careful not to rely on any table methods that +require data, such as :meth:`~horizon.tables.DataTable.get_object_display` or +:meth:`~horizon.tables.DataTable.get_object_by_id`. The advantage of preemptive +actions is that you can avoid having to do all the processing, API calls, etc. +associated with loading data into the table for actions which don't require +access to that information. diff --git a/dashboard/doc/source/topics/testing.rst b/dashboard/doc/source/topics/testing.rst new file mode 100644 index 00000000..e8afb4d3 --- /dev/null +++ b/dashboard/doc/source/topics/testing.rst @@ -0,0 +1,276 @@ +=================== +Testing Topic Guide +=================== + +Having good tests in place is absolutely critical for ensuring a stable, +maintainable codebase. Hopefully that doesn't need any more explanation. + +However, what defines a "good" test is not always obvious, and there are +a lot of common pitfalls that can easily shoot your test suite in the +foot. + +If you already know everything about testing but are fed up with trying to +debug why a specific test failed, you can skip the intro and jump +stright to :ref:`debugging_unit_tests`. + +An overview of testing +====================== + +There are three main types of tests, each with their associated pros and cons: + +Unit tests +---------- + +These are isolated, stand-alone tests with no external dependencies. They are +written from the a perspective of "knowing the code", and test the assumptions +of the codebase and the developer. + +Pros: + +* Generally lightweight and fast. +* Can be run anywhere, anytime since they have no external dependencies. + +Cons: + +* Easy to be lax in writing them, or lazy in constructing them. +* Can't test interactions with live external services. + +Functional tests +---------------- + +These are generally also isolated tests, though sometimes they may interact +with other services running locally. The key difference between functional +tests and unit tests, however, is that functional tests are written from the +perspective of the user (who knows nothing about the code) and only knows +what they put in and what they get back. Essentially this is a higher-level +testing of "does the result match the spec?". + +Pros: + +* Ensures that your code *always* meets the stated functional requirements. +* Verifies things from an "end user" perspective, which helps to ensure + a high-quality experience. +* Designing your code with a functional testing perspective in mind helps + keep a higher-level viewpoint in mind. + +Cons: + +* Requires an additional layer of thinking to define functional requirements + in terms of inputs and outputs. +* Often requires writing a separate set of tests and/or using a different + testing framework from your unit tests. +* Don't offer any insight into the quality or status of the underlying code, + only verifies that it works or it doesn't. + +Integration Tests +----------------- + +This layer of testing involves testing all of the components that your +codebase interacts with or relies on in conjunction. This is equivalent to +"live" testing, but in a repeatable manner. + +Pros: + +* Catches *many* bugs that unit and functional tests will not. +* Doesn't rely on assumptions about the inputs and outputs. +* Will warn you when changes in external components break your code. + +Cons: + +* Difficult and time-consuming to create a repeatable test environment. +* Did I mention that setting it up is a pain? + +So what should I write? +----------------------- + +A few simple guidelines: + +#. Every bug fix should have a regression test. Period. + +#. When writing a new feature, think about writing unit tests to verify + the behavior step-by-step as you write the feature. Every time you'd + go to run your code by hand and verify it manually, think "could I + write a test to do this instead?". That way when the feature is done + and you're ready to commit it you've already got a whole set of tests + that are more thorough than anything you'd write after the fact. + +#. Write tests that hit every view in your application. Even if they + don't assert a single thing about the code, it tells you that your + users aren't getting fatal errors just by interacting with your code. + +What makes a good unit test? +============================ + +Limiting our focus just to unit tests, there are a number of things you can +do to make your unit tests as useful, maintainable, and unburdensome as +possible. + +Test data +--------- + +Use a single, consistent set of test data. Grow it over time, but do everything +you can not to fragment it. It quickly becomes unmaintainable and perniciously +out-of-sync with reality. + +Make your test data as accurate to reality as possible. Supply *all* the +attributes of an object, provide objects in all the various states you may want +to test. + +If you do the first suggestion above *first* it makes the second one far less +painful. Write once, use everywhere. + +To make your life even easier, if your codebase doesn't have a built-in +ORM-like function to manage your test data you can consider buidling (or +borrowing) one yourself. Being able to do simple retrieval queries on your +test data is incredibly valuable. + +Mocking +------- + +Mocking is the practice of providing stand-ins for objects or pieces of code +you don't need to test. While convenient, they should be used with *extreme* +caution. + +Why? Because overuse of mocks can rapidly land you in a situation where you're +not testing any real code. All you've done is verified that your mocking +framework returns what you tell it to. This problem can be very tricky to +recognize, since you may be mocking things in ``setUp`` methods, other modules, +etc. + +A good rule of thumb is to mock as close to the source as possible. If you have +a function call that calls an external API in a view , mock out the external +API, not the whole function. If you mock the whole function you've suddenly +lost test coverage for an entire chunk of code *inside* your codebase. Cut the +ties cleanly right where your system ends and the external world begins. + +Similarly, don't mock return values when you could construct a real return +value of the correct type with the correct attributes. You're just adding +another point of potential failure by exercising your mocking framework instead +of real code. Following the suggestions for testing above will make this a lot +less burdensome. + +Assertions and verification +--------------------------- + +Think long and hard about what you really want to verify in your unit test. In +particular, think about what custom logic your code executes. + +A common pitfall is to take a known test object, pass it through your code, +and then verify the properties of that object on the output. This is all well +and good, except if you're verifying properties that were untouched by your +code. What you want to check are the pieces that were *changed*, *added*, or +*removed*. Don't check the object's id attribute unless you have reason to +suspect it's not the object you started with. But if you added a new attribute +to it, be damn sure you verify that came out right. + +It's also very common to avoid testing things you really care about because +it's more difficult. Verifying that the proper messages were displayed to the +user after an action, testing for form errors, making sure exception handling +is tested... these types of things aren't always easy, but they're extremely +necessary. + +To that end, Horizon includes several custom assertions to make these tasks +easier. :meth:`~horizon.test.helpers.TestCase.assertNoFormErrors`, +:meth:`~horizon.test.helpers.TestCase.assertMessageCount`, and +:meth:`~horizon.test.helpers.TestCase.asertNoMessages` all exist for exactly +these purposes. Moreover, they provide useful output when things go wrong so +you're not left scratching your head wondering why your view test didn't +redirect as expected when you posted a form. + +.. _debugging_unit_tests: + +Debugging Unit Tests +==================== + +Tips and tricks +--------------- + +#. Use :meth:`~horizon.test.helpers.TestCase.assertNoFormErrors` immediately + after your ``client.post`` call for tests that handle form views. This will + immediately fail if your form POST failed due to a validation error and + tell you what the error was. + +#. Use :meth:`~horizon.test.helpers.TestCase.assertMessageCount` and + :meth:`~horizon.test.helpers.TestCase.asertNoMessages` when a piece of code + is failing inexplicably. Since the core error handlers attach user-facing + error messages (and since the core logging is silenced during test runs) + these methods give you the dual benefit of verifying the output you expect + while clearly showing you the problematic error message if they fail. + +#. Use Python's ``pdb`` module liberally. Many people don't realize it works + just as well in a test case as it does in a live view. Simply inserting + ``import pdb; pdb.set_trace()`` anywhere in your codebase will drop the + interpreter into an interactive shell so you can explore your test + environment and see which of your assumptions about the code isn't, + in fact, flawlessly correct. + +Common pitfalls +--------------- + +There are a number of typical (and non-obvious) ways to break the unit tests. +Some common things to look for: + +#. Make sure you stub out the method exactly as it's called in the code + being tested. For example, if your real code calls + ``api.keystone.tenant_get``, stubbing out ``api.tenant_get`` (available + for legacy reasons) will fail. + +#. When defining the expected input to a stubbed call, make sure the + arguments are *identical*, this includes ``str`` vs. ``int`` differences. + +#. Make sure your test data are completely in line with the expected inputs. + Again, ``str`` vs. ``int`` or missing properties on test objects will + kill your tests. + +#. Make sure there's nothing amiss in your templates (particularly the + ``{% url %}`` tag and its arguments). This often comes up when refactoring + views or renaming context variables. It can easily result in errors that + you might not stumble across while clicking around the development server. + +#. Make sure you're not redirecting to views that no longer exist, e.g. + the ``index`` view for a panel that got combined (such as instances & + volumes). + +#. Make sure your mock calls are in order before calling ``mox.ReplayAll``. + The order matters. + +#. Make sure you repeat any stubbed out method calls that happen more than + once. They don't automatically repeat, you have to explicitly define them. + While this is a nuisance, it makes you acutely aware of how many API + calls are involved in a particular function. + +Understanding the output from ``mox`` +------------------------------------- + +Horizon uses ``mox`` as its mocking framework of choice, and while it +offers many nice features, its output when a test fails can be quite +mysterious. + +Unexpected Method Call +~~~~~~~~~~~~~~~~~~~~~~ + +This occurs when you stubbed out a piece of code, and it was subsequently +called in a way that you didn't specify it would be. There are two reasons +this tends to come up: + +#. You defined the expected call, but a subtle difference crept in. This + may be a string versus integer difference, a string versus unicode + difference, a slightly off date/time, or passing a name instead of an id. + +#. The method is actually being called *multiple times*. Since mox uses + a call stack internally, it simply pops off the expected method calls to + verify them. That means once a call is used once, it's gone. An easy way + to see if this is the case is simply to copy and paste your method call a + second time to see if the error changes. If it does, that means your method + is being called more times than you think it is. + +Expected Method Never Called +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This one is the opposite of the unexpected method call. This one means you +tol mox to expect a call and it didn't happen. This is almost always the +result of an error in the conditions of the test. Using the +:meth:`~horizon.test.helpers.TestCase.assertNoFormErrors` and +:meth:`~horizon.test.helpers.TestCase.assertMessageCount` will make it readily +apparent what the problem is in the majority of cases. If not, then use ``pdb`` +and start interrupting the code flow to see where things are getting off track. diff --git a/dashboard/doc/source/topics/tutorial.rst b/dashboard/doc/source/topics/tutorial.rst new file mode 100644 index 00000000..056eaa3e --- /dev/null +++ b/dashboard/doc/source/topics/tutorial.rst @@ -0,0 +1,545 @@ +=================== +Building on Horizon +=================== + +This tutorial covers how to use the various components in Horizon to build +an example dashboard and panel with a data table and tabs. + +As an example, we'll build on the Nova instances API to create a new and novel +"visualizations" dashboard with a "flocking" panel that presents the instance +data in a different manner. + +You can find a reference implementation of the code being described here +on github at https://github.com/gabrielhurley/horizon_demo. + +.. note:: + + There are a variety of other resources which may be helpful to read first, + since this is a more advanced tutorial. For example, you may want to start + with the :doc:`Horizon quickstart guide ` or the + `Django tutorial`_. + + .. _Django tutorial: https://docs.djangoproject.com/en/1.4/intro/tutorial01/ + + +Creating a dashboard +==================== + +.. note:: + + It is perfectly valid to create a panel without a dashboard, and + incorporate it into an existing dashboard. See the section + :ref:`overrides ` later in this document. + +The quick version +----------------- + +Horizon provides a custom management command to create a typical base +dashboard structure for you. The following command generates most of the +boilerplate code explained below:: + + ./run_tests.sh -m startdash visualizations + +It's still recommended that you read the rest of this section to understand +what that command creates and why. + +Structure +--------- + +The recommended structure for a dashboard (or panel) follows suit with the +typical Django application layout. We'll name our dashboard "visualizations":: + + visualizations + |--__init__.py + |--dashboard.py + |--templates/ + |--static/ + +The ``dashboard.py`` module will contain our dashboard class for use by +Horizon; the ``templates`` and ``static`` directories give us homes for our +Django template files and static media respectively. + +Within the ``static`` and ``templates`` directories it's generally good to +namespace your files like so:: + + templates/ + |--visualizations/ + static/ + |--visualizations/ + |--css/ + |--js/ + |--img/ + +With those files and directories in place, we can move on to writing our +dashboard class. + + +Defining a dashboard +-------------------- + +A dashboard class can be incredibly simple (about 3 lines at minimum), +defining nothing more than a name and a slug:: + + import horizon + + class VizDash(horizon.Dashboard): + name = _("Visualizations") + slug = "visualizations" + +In practice, a dashboard class will usually contain more information, such as a +list of panels, which panel is the default, and any permissions required to +access this dashboard:: + + class VizDash(horizon.Dashboard): + name = _("Visualizations") + slug = "visualizations" + panels = ('flocking',) + default_panel = 'flocking' + permissions = ('openstack.roles.admin',) + +Building from that previous example we may also want to define a grouping of +panels which share a common theme and have a sub-heading in the navigation:: + + class InstanceVisualizations(horizon.PanelGroup): + slug = "instance_visualizations" + name = _("Instance Visualizations") + panels = ('flocking',) + + + class VizDash(horizon.Dashboard): + name = _("Visualizations") + slug = "visualizations" + panels = (InstanceVisualizations,) + default_panel = 'flocking' + permissions = ('openstack.roles.admin',) + +The ``PanelGroup`` can be added to the dashboard class' ``panels`` list +just like the slug of the panel can. + +Once our dashboard class is complete, all we need to do is register it:: + + horizon.register(VizDash) + +The typical place for that would be the bottom of the ``dashboard.py`` file, +but it could also go elsewhere, such as in an override file (see below). + + +Creating a panel +================ + +Now that we have our dashboard written, we can also create our panel. We'll +call it "flocking". + +.. note:: + + You don't need to write a custom dashboard to add a panel. The structure + here is for the sake of completeness in the tutorial. + +The quick version +----------------- + +Horizon provides a custom management command to create a typical base +panel structure for you. The following command generates most of the +boilerplate code explained below:: + + ./run_tests.sh -m startpanel flocking --dashboard=visualizations --target=auto + +The ``dashboard`` argument is required, and tells the command which dashboard +this panel will be registered with. The ``target`` argument is optional, and +respects ``auto`` as a special value which means that the files for the panel +should be created inside the dashboard module as opposed to the current +directory (the default). + +It's still recommended that you read the rest of this section to understand +what that command creates and why. + +Structure +--------- + +A panel is a relatively flat structure with the exception that templates +for a panel in a dashboard live in the dashboard's ``templates`` directory +rather than in the panel's ``templates`` directory. Continuing our +vizulaization/flocking example, let's see what the looks like:: + + # stand-alone panel structure + flocking/ + |--__init__.py + |--panel.py + |--urls.py + |--views.py + |--templates/ + |--flocking/ + |--index.html + + # panel-in-a-dashboard structure + visualizations/ + |--__init__.py + |--dashboard.py + |--flocking/ + |--__init__.py + |--panel.py + |--urls.py + |--views.py + |--templates/ + |--visualizations/ + |--flocking/ + |--index.html + +That follows standard Django namespacing conventions for apps and submodules +within apps. It also works cleanly with Django's automatic template discovery +in both cases. + +Defining a panel +---------------- + +The ``panel.py`` file referenced above has a special meaning. Within a +dashboard, any module name listed in the ``panels`` attribute on the +dashboard class will be auto-discovered by looking for ``panel.py`` file +in a corresponding directory (the details are a bit magical, but have been +thoroughly vetted in Django's admin codebase). + +Inside the ``panel.py`` module we define our ``Panel`` class:: + + class Flocking(horizon.Panel): + name = _("Flocking") + slug = 'flocking' + +Simple, right? Once we've defined it, we register it with the dashboard:: + + from visualizations import dashboard + + dashboard.VizDash.register(Flocking) + +Easy! There are more options you can set to customize the ``Panel`` class, but +it makes some intelligent guesses about what the defaults should be. + +URLs +---- + +One of the intelligent assumptions the ``Panel`` class makes is that it can +find a ``urls.py`` file in your panel directory which will define a view named +``index`` that handles the default view for that panel. This is what your +``urls.py`` file might look like:: + + from django.conf.urls.defaults import patterns, url + from .views import IndexView + + urlpatterns = patterns('', + url(r'^$', IndexView.as_view(), name='index') + ) + +There's nothing there that isn't 100% standard Django code. This example +(and Horizon in general) uses the class-based views introduced in Django 1.3 +to make code more reusable. Hence the view class is imported in the example +above, and the ``as_view()`` method is called in the URL pattern. + +This, of course, presumes you have a view class, and takes us into the meat +of writing a ``Panel``. + + +Tables, Tabs, and Views +----------------------- + +Now we get to the really exciting parts; everything before this was structural. + +Starting with the high-level view, our end goal is to create a view (our +``IndexView`` class referenced above) which uses Horizon's ``DataTable`` +class to display data and Horizon's ``TabGroup`` class to give us a +user-friendly tabbed interface in the browser. + +We'll start with the table, combine that with the tabs, and then build our +view from the pieces. + +Defining a table +~~~~~~~~~~~~~~~~ + +Horizon provides a :class:`~horizon.tables.DataTable` class which simplifies +the vast majority of displaying data to an end-user. We're just going to skim +the surface here, but it has a tremendous number of capabilities. + +In this case, we're going to be presenting data about tables, so let's start +defining our table (and a ``tables.py`` module:: + + from horizon import tables + + class FlockingInstancesTable(tables.DataTable): + host = tables.Column("OS-EXT-SRV-ATTR:host", verbose_name=_("Host")) + tenant = tables.Column('tenant_name', verbose_name=_("Tenant")) + user = tables.Column('user_name', verbose_name=_("user")) + vcpus = tables.Column('flavor_vcpus', verbose_name=_("VCPUs")) + memory = tables.Column('flavor_memory', verbose_name=_("Memory")) + age = tables.Column('age', verbose_name=_("Age")) + + class Meta: + name = "instances" + verbose_name = _("Instances") + +There are several things going on here... we created a table subclass, +and defined six columns on it. Each of those columns defines what attribute +it accesses on the instance object as the first argument, and since we like to +make everything translatable, we give each column a ``verbose_name`` that's +marked for translation. + +Lastly, we added a ``Meta`` class which defines some properties about our +table, notably it's (translatable) verbose name, and a semi-unique "slug"-like +name to identify it. + +.. note:: + + This is a slight simplification from the reality of how the instance + object is actually structured. In reality, accessing the flavor, tenant, + and user attributes on it requires an additional step. This code can be + seen in the example code available on github. + +Defining tabs +~~~~~~~~~~~~~ + +So we have a table, ready to receive our data. We could go straight to a view +from here, but we can think bigger. In this case we're also going to use +Horizon's :class:`~horizon.tabs.TabGroup` class. This gives us a clean, +no-fuss tabbed interface to display both our visualization and, optionally, +our data table. + +First off, let's make a tab for our visualization:: + + class VizTab(tabs.Tab): + name = _("Visualization") + slug = "viz" + template_name = "visualizations/flocking/_flocking.html" + + def get_context_data(self, request): + return None + +This is about as simple as you can get. Since our visualization will +ultiimately use AJAX to load it's data we don't need to pass any context +to the template, and all we need to define is the name and which template +it should use. + +Now, we also need a tab for our data table:: + + from .tables import FlockingInstancesTable + + class DataTab(tabs.TableTab): + name = _("Data") + slug = "data" + table_classes = (FlockingInstancesTable,) + template_name = "horizon/common/_detail_table.html" + preload = False + + def get_instances_data(self): + try: + instances = utils.get_instances_data(self.tab_group.request) + except: + instances = [] + exceptions.handle(self.tab_group.request, + _('Unable to retrieve instance list.')) + return instances + +This tab gets a little more complicated. Foremost, it's a special type of +tab--one that handles data tables (and all their associated features)--and +it also uses the ``preload`` attribute to specify that this tab shouldn't +be loaded by default. It will instead be loaded via AJAX when someone clicks +on it, saving us on API calls in the vast majority of cases. + +Lastly, this code introduces the concept of error handling in Horizon. +The :func:`horizon.exceptions.handle` function is a centralized error +handling mechanism that takes all the guess-work and inconsistency out of +dealing with exceptions from the API. Use it everywhere. + +Tying it together in a view +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There are lots of pre-built class-based views in Horizon. We try to provide +starting points for all the common combinations of components. + +In this case we want a starting view type that works with both tabs and +tables... that'd be the :class:`~horizon.tabs.TabbedTableView` class. It takes +the best of the dynamic delayed-loading capabilities tab groups provide and +mixes in the actions and AJAX-updating that tables are capable of with almost +no work on the user's end. Let's see what the code would look like:: + + from .tables import FlockingInstancesTable + from .tabs import FlockingTabs + + class IndexView(tabs.TabbedTableView): + tab_group_class = FlockingTabs + table_class = FlockingInstancesTable + template_name = 'visualizations/flocking/index.html' + +That would get us 100% of the way to what we need if this particular +demo didn't involve an extra AJAX call to fetch back our visualization +data via AJAX. Because of that we need to override the class' ``get()`` +method to return the right data for an AJAX call:: + + from .tables import FlockingInstancesTable + from .tabs import FlockingTabs + + class IndexView(tabs.TabbedTableView): + tab_group_class = FlockingTabs + table_class = FlockingInstancesTable + template_name = 'visualizations/flocking/index.html' + + def get(self, request, *args, **kwargs): + if self.request.is_ajax() and self.request.GET.get("json", False): + try: + instances = utils.get_instances_data(self.request) + except: + instances = [] + exceptions.handle(request, + _('Unable to retrieve instance list.')) + data = json.dumps([i._apiresource._info for i in instances]) + return http.HttpResponse(data) + else: + return super(IndexView, self).get(request, *args, **kwargs) + +In this instance, we override the ``get()`` method such that if it's an +AJAX request and has the GET parameter we're looking for, it returns our +instance data in JSON format; otherwise it simply returns the view function +as per the usual. + +The template +~~~~~~~~~~~~ + +We need three templates here: one for the view, and one for each of our two +tabs. The view template (in this case) can inherit from one of the other +dashboards:: + + {% extends 'syspanel/base.html' %} + {% load i18n %} + {% block title %}{% trans "Flocking" %}{% endblock %} + + {% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Flocking") %} + {% endblock page_header %} + + {% block syspanel_main %} +
      +
      + {{ tab_group.render }} +
      +
      + {% endblock %} + +This gives us a custom page title, a header, and render our tab group provided +by the view. + +For the tabs, the one using the table is handled by a reusable template, +``"horizon/common/_detail_table.html"``. This is appropriate for any tab that +only displays a single table. + +The second tab is a bit of secret sauce for the visualization, but it's still +quite simple and can be investigated in the github example. + +The takeaway here is that each tab needs a template associated with it. + +With all our code in place, the only thing left to do is to integrated it into +our OpenStack Dashboard site. + +Setting up a project +==================== + +The vast majority of people will just customize the OpenStack Dashboard +example project that ships with Horizon. As such, this tutorial will +start from that and just illustrate the bits that can be customized. + +Structure +--------- + +A site built on Horizon takes the form of a very typical Django project:: + + site/ + |--__init__.py + |--manage.py + |--demo_dashboard/ + |--__init__.py + |--models.py # required for Django even if unused + |--settings.py + |--templates/ + |--static/ + +The key bits here are that ``demo_dashboard`` is on our python path, and that +the `settings.py`` file here will contain our customized Horizon config. + +The settings file +----------------- + +There are several key things you will generally want to customiz in your +site's settings file: specifying custom dashboards and panels, catching your +client's exception classes, and (possibly) specifying a file for advanced +overrides. + +Specifying dashboards +~~~~~~~~~~~~~~~~~~~~~ + +The most basic thing to do is to add your own custom dashboard using the +``HORIZON_CONFIG`` dictionary in the settings file:: + + HORIZON_CONFIG = { + 'dashboards': ('nova', 'syspanel', 'visualizations', 'settings',), + } + +In this case, we've taken the default Horizon ``'dashboards'`` config and +added our ``visualizations`` dashboard to it. Note that the name here is the +name of the dashboard's module on the python path. It will find our +``dashboard.py`` file inside of it and load both the dashboard and its panels +automatically from there. + +Error handling +~~~~~~~~~~~~~~ + +Adding custom error handler for your API client is quite easy. While it's not +necessary for this example, it would be done by customizing the +``'exceptions'`` value in the ``HORIZON_CONFIG`` dictionary:: + + import my_api.exceptions as my_api + + 'exceptions': {'recoverable': [my_api.Error, + my_api.ClientConnectionError], + 'not_found': [my_api.NotFound], + 'unauthorized': [my_api.NotAuthorized]}, + +.. _overrides: + +Override file +~~~~~~~~~~~~~ + +The override file is the "god-mode" dashboard editor. The hook for this file +sits right between the automatic discovery mechanisms and the final setup +routines for the entire site. By specifying an override file you can alter +any behavior you like in existing code. This tutorial won't go in-depth, +but let's just say that with great power comes great responsibility. + +To specify am override file, you set the ``'customization_module'`` value in +the ``HORIZON_CONFIG`` dictionary to the dotted python path of your +override module:: + + HORIZON_CONFIG = { + 'customization_module': 'demo_dashboard.overrides' + } + +This file is capable of adding dashboards, adding panels to existing +dashboards, renaming existing dashboards and panels (or altering other +attributes on them), removing panels from existing dashboards, and so on. + +We could say more, but it only gets more dangerous... + +Conclusion +========== + +Sadly, the cake was a lie. The information in this "tutorial" was never +meant to leave you with a working dashboard. It's close. But there's +waaaaaay too much javascript involved in the visualization to cover it all +here, and it'd be irrelevant to Horizon anyway. + +If you want to see the finished product, check out the github example +referenced at the beginning of this tutorial. + +Clone the repository and simply run ``./run_tests.sh --runserver``. That'll +give you a 100% working dashboard that uses every technique in this tutorial. + +What you've learned here, however, is the fundamentals of almost everything +you need to know to start writing interfaces for your own project based on the +components Horizon provides. + +If you have questions, or feedback on how this tutorial could be improved, +please feel free to pass them along! diff --git a/dashboard/horizon/__init__.py b/dashboard/horizon/__init__.py new file mode 100644 index 00000000..01b51d66 --- /dev/null +++ b/dashboard/horizon/__init__.py @@ -0,0 +1,49 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +""" The Horizon interface. + +Contains the core Horizon classes--:class:`~horizon.Dashboard` and +:class:`horizon.Panel`--the dynamic URLconf for Horizon, and common interface +methods like :func:`~horizon.register` and :func:`~horizon.unregister`. + +""" +# Because this module is compiled by setup.py before Django may be installed +# in the environment we try importing Django and issue a warning but move on +# should that fail. +Horizon = None +try: + from horizon.base import Horizon, Dashboard, Panel, PanelGroup +except ImportError: + import warnings + + def simple_warn(message, category, filename, lineno, file=None, line=None): + return '%s: %s' % (category.__name__, message) + + msg = ("Could not import Horizon dependencies. " + "This is normal during installation.\n") + warnings.formatwarning = simple_warn + warnings.warn(msg, Warning) + +if Horizon: + register = Horizon.register + unregister = Horizon.unregister + get_absolute_url = Horizon.get_absolute_url + get_user_home = Horizon.get_user_home + get_dashboard = Horizon.get_dashboard + get_default_dashboard = Horizon.get_default_dashboard + get_dashboards = Horizon.get_dashboards + urls = Horizon._lazy_urls diff --git a/dashboard/horizon/base.py b/dashboard/horizon/base.py new file mode 100644 index 00000000..a1337554 --- /dev/null +++ b/dashboard/horizon/base.py @@ -0,0 +1,788 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +""" +Contains the core classes and functionality that makes Horizon what it is. +This module is considered internal, and should not be relied on directly. + +Public APIs are made available through the :mod:`horizon` module and +the classes contained therein. +""" + +import collections +import copy +import inspect +import logging +import os + +from django.conf import settings +from django.conf.urls.defaults import patterns, url, include +from django.core.exceptions import ImproperlyConfigured +from django.core.urlresolvers import reverse +from django.utils.datastructures import SortedDict +from django.utils.functional import SimpleLazyObject +from django.utils.importlib import import_module +from django.utils.module_loading import module_has_submodule +from django.utils.translation import ugettext as _ + +from horizon import loaders +from horizon import conf +from horizon.decorators import require_auth, require_perms, _current_component + + +LOG = logging.getLogger(__name__) + + +def _decorate_urlconf(urlpatterns, decorator, *args, **kwargs): + for pattern in urlpatterns: + if getattr(pattern, 'callback', None): + pattern._callback = decorator(pattern.callback, *args, **kwargs) + if getattr(pattern, 'url_patterns', []): + _decorate_urlconf(pattern.url_patterns, decorator, *args, **kwargs) + + +class NotRegistered(Exception): + pass + + +class HorizonComponent(object): + def __init__(self): + super(HorizonComponent, self).__init__() + if not self.slug: + raise ImproperlyConfigured('Every %s must have a slug.' + % self.__class__) + + def __unicode__(self): + name = getattr(self, 'name', u"Unnamed %s" % self.__class__.__name__) + return unicode(name) + + def _get_default_urlpatterns(self): + package_string = '.'.join(self.__module__.split('.')[:-1]) + if getattr(self, 'urls', None): + try: + mod = import_module('.%s' % self.urls, package_string) + except ImportError: + mod = import_module(self.urls) + urlpatterns = mod.urlpatterns + else: + # Try importing a urls.py from the dashboard package + if module_has_submodule(import_module(package_string), 'urls'): + urls_mod = import_module('.urls', package_string) + urlpatterns = urls_mod.urlpatterns + else: + urlpatterns = patterns('') + return urlpatterns + + +class Registry(object): + def __init__(self): + self._registry = {} + if not getattr(self, '_registerable_class', None): + raise ImproperlyConfigured('Subclasses of Registry must set a ' + '"_registerable_class" property.') + + def _register(self, cls): + """Registers the given class. + + If the specified class is already registered then it is ignored. + """ + if not inspect.isclass(cls): + raise ValueError('Only classes may be registered.') + elif not issubclass(cls, self._registerable_class): + raise ValueError('Only %s classes or subclasses may be registered.' + % self._registerable_class.__name__) + + if cls not in self._registry: + cls._registered_with = self + self._registry[cls] = cls() + + return self._registry[cls] + + def _unregister(self, cls): + """Unregisters the given class. + + If the specified class isn't registered, ``NotRegistered`` will + be raised. + """ + if not issubclass(cls, self._registerable_class): + raise ValueError('Only %s classes or subclasses may be ' + 'unregistered.' % self._registerable_class) + + if cls not in self._registry.keys(): + raise NotRegistered('%s is not registered' % cls) + + del self._registry[cls] + + return True + + def _registered(self, cls): + if inspect.isclass(cls) and issubclass(cls, self._registerable_class): + found = self._registry.get(cls, None) + if found: + return found + else: + # Allow for fetching by slugs as well. + for registered in self._registry.values(): + if registered.slug == cls: + return registered + class_name = self._registerable_class.__name__ + if hasattr(self, "_registered_with"): + parent = self._registered_with._registerable_class.__name__ + raise NotRegistered('%(type)s with slug "%(slug)s" is not ' + 'registered with %(parent)s "%(name)s".' + % {"type": class_name, + "slug": cls, + "parent": parent, + "name": self.slug}) + else: + slug = getattr(cls, "slug", cls) + raise NotRegistered('%(type)s with slug "%(slug)s" is not ' + 'registered.' % {"type": class_name, + "slug": slug}) + + +class Panel(HorizonComponent): + """ A base class for defining Horizon dashboard panels. + + All Horizon dashboard panels should extend from this class. It provides + the appropriate hooks for automatically constructing URLconfs, and + providing permission-based access control. + + .. attribute:: name + + The name of the panel. This will be displayed in the + auto-generated navigation and various other places. + Default: ``''``. + + .. attribute:: slug + + A unique "short name" for the panel. The slug is used as + a component of the URL path for the panel. Default: ``''``. + + .. attribute:: permissions + + A list of permission names, all of which a user must possess in order + to access any view associated with this panel. This attribute + is combined cumulatively with any permissions required on the + ``Dashboard`` class with which it is registered. + + .. attribute:: urls + + Path to a URLconf of views for this panel using dotted Python + notation. If no value is specified, a file called ``urls.py`` + living in the same package as the ``panel.py`` file is used. + Default: ``None``. + + .. attribute:: nav + .. method:: nav(context) + + The ``nav`` attribute can be either boolean value or a callable + which accepts a ``RequestContext`` object as a single argument + to control whether or not this panel should appear in + automatically-generated navigation. Default: ``True``. + + .. attribute:: index_url_name + + The ``name`` argument for the URL pattern which corresponds to + the index view for this ``Panel``. This is the view that + :meth:`.Panel.get_absolute_url` will attempt to reverse. + """ + name = '' + slug = '' + urls = None + nav = True + index_url_name = "index" + + def __repr__(self): + return "" % self.slug + + def get_absolute_url(self): + """ Returns the default URL for this panel. + + The default URL is defined as the URL pattern with ``name="index"`` in + the URLconf for this panel. + """ + try: + return reverse('horizon:%s:%s:%s' % (self._registered_with.slug, + self.slug, + self.index_url_name)) + except Exception as exc: + # Logging here since this will often be called in a template + # where the exception would be hidden. + LOG.info("Error reversing absolute URL for %s: %s" % (self, exc)) + raise + + @property + def _decorated_urls(self): + urlpatterns = self._get_default_urlpatterns() + + # Apply access controls to all views in the patterns + permissions = getattr(self, 'permissions', []) + _decorate_urlconf(urlpatterns, require_perms, permissions) + _decorate_urlconf(urlpatterns, _current_component, panel=self) + + # Return the three arguments to django.conf.urls.defaults.include + return urlpatterns, self.slug, self.slug + + +class PanelGroup(object): + """ A container for a set of :class:`~horizon.Panel` classes. + + When iterated, it will yield each of the ``Panel`` instances it + contains. + + .. attribute:: slug + + A unique string to identify this panel group. Required. + + .. attribute:: name + + A user-friendly name which will be used as the group heading in + places such as the navigation. Default: ``None``. + + .. attribute:: panels + + A list of panel module names which should be contained within this + grouping. + """ + def __init__(self, dashboard, slug=None, name=None, panels=None): + self.dashboard = dashboard + self.slug = slug or getattr(self, "slug", "default") + self.name = name or getattr(self, "name", None) + # Our panels must be mutable so it can be extended by others. + self.panels = list(panels or getattr(self, "panels", [])) + + def __repr__(self): + return "<%s: %s>" % (self.__class__.__name__, self.slug) + + def __unicode__(self): + return self.name + + def __iter__(self): + panel_instances = [] + for name in self.panels: + try: + panel_instances.append(self.dashboard.get_panel(name)) + except NotRegistered, e: + LOG.debug(e) + return iter(panel_instances) + + +class Dashboard(Registry, HorizonComponent): + """ A base class for defining Horizon dashboards. + + All Horizon dashboards should extend from this base class. It provides the + appropriate hooks for automatic discovery of :class:`~horizon.Panel` + modules, automatically constructing URLconfs, and providing + permission-based access control. + + .. attribute:: name + + The name of the dashboard. This will be displayed in the + auto-generated navigation and various other places. + Default: ``''``. + + .. attribute:: slug + + A unique "short name" for the dashboard. The slug is used as + a component of the URL path for the dashboard. Default: ``''``. + + .. attribute:: panels + + The ``panels`` attribute can be either a flat list containing the name + of each panel **module** which should be loaded as part of this + dashboard, or a list of :class:`~horizon.PanelGroup` classes which + define groups of panels as in the following example:: + + class SystemPanels(horizon.PanelGroup): + slug = "syspanel" + name = _("System Panel") + panels = ('overview', 'instances', ...) + + class Syspanel(horizon.Dashboard): + panels = (SystemPanels,) + + Automatically generated navigation will use the order of the + modules in this attribute. + + Default: ``[]``. + + .. warning:: + + The values for this attribute should not correspond to the + :attr:`~.Panel.name` attributes of the ``Panel`` classes. + They should be the names of the Python modules in which the + ``panel.py`` files live. This is used for the automatic + loading and registration of ``Panel`` classes much like + Django's ``ModelAdmin`` machinery. + + Panel modules must be listed in ``panels`` in order to be + discovered by the automatic registration mechanism. + + .. attribute:: default_panel + + The name of the panel which should be treated as the default + panel for the dashboard, i.e. when you visit the root URL + for this dashboard, that's the panel that is displayed. + Default: ``None``. + + .. attribute:: permissions + + A list of permission names, all of which a user must possess in order + to access any panel registered with this dashboard. This attribute + is combined cumulatively with any permissions required on individual + :class:`~horizon.Panel` classes. + + .. attribute:: urls + + Optional path to a URLconf of additional views for this dashboard + which are not connected to specific panels. Default: ``None``. + + .. attribute:: nav + + Optional boolean to control whether or not this dashboard should + appear in automatically-generated navigation. Default: ``True``. + + .. attribute:: supports_tenants + + Optional boolean that indicates whether or not this dashboard includes + support for projects/tenants. If set to ``True`` this dashboard's + navigation will include a UI element that allows the user to select + project/tenant. Default: ``False``. + + .. attribute:: public + + Boolean value to determine whether this dashboard can be viewed + without being logged in. Defaults to ``False``. + """ + _registerable_class = Panel + name = '' + slug = '' + urls = None + panels = [] + default_panel = None + nav = True + supports_tenants = False + public = False + + def __repr__(self): + return "" % self.slug + + def __init__(self, *args, **kwargs): + super(Dashboard, self).__init__(*args, **kwargs) + self._panel_groups = None + + def get_panel(self, panel): + """ + Returns the specified :class:`~horizon.Panel` instance registered + with this dashboard. + """ + return self._registered(panel) + + def get_panels(self): + """ + Returns the :class:`~horizon.Panel` instances registered with this + dashboard in order, without any panel groupings. + """ + all_panels = [] + panel_groups = self.get_panel_groups() + for panel_group in panel_groups.values(): + all_panels.extend(panel_group) + return all_panels + + def get_panel_group(self, slug): + return self._panel_groups[slug] + + def get_panel_groups(self): + registered = copy.copy(self._registry) + panel_groups = [] + + # Gather our known panels + for panel_group in self._panel_groups.values(): + for panel in panel_group: + registered.pop(panel.__class__) + panel_groups.append((panel_group.slug, panel_group)) + + # Deal with leftovers (such as add-on registrations) + if len(registered): + slugs = [panel.slug for panel in registered.values()] + new_group = PanelGroup(self, + slug="other", + name=_("Other"), + panels=slugs) + panel_groups.append((new_group.slug, new_group)) + return SortedDict(panel_groups) + + def get_absolute_url(self): + """ Returns the default URL for this dashboard. + + The default URL is defined as the URL pattern with ``name="index"`` + in the URLconf for the :class:`~horizon.Panel` specified by + :attr:`~horizon.Dashboard.default_panel`. + """ + try: + return self._registered(self.default_panel).get_absolute_url() + except: + # Logging here since this will often be called in a template + # where the exception would be hidden. + LOG.exception("Error reversing absolute URL for %s." % self) + raise + + @property + def _decorated_urls(self): + urlpatterns = self._get_default_urlpatterns() + + default_panel = None + + # Add in each panel's views except for the default view. + for panel in self._registry.values(): + if panel.slug == self.default_panel: + default_panel = panel + continue + urlpatterns += patterns('', + url(r'^%s/' % panel.slug, include(panel._decorated_urls))) + # Now the default view, which should come last + if not default_panel: + raise NotRegistered('The default panel "%s" is not registered.' + % self.default_panel) + urlpatterns += patterns('', + url(r'', include(default_panel._decorated_urls))) + + # Require login if not public. + if not self.public: + _decorate_urlconf(urlpatterns, require_auth) + # Apply access controls to all views in the patterns + permissions = getattr(self, 'permissions', []) + _decorate_urlconf(urlpatterns, require_perms, permissions) + _decorate_urlconf(urlpatterns, _current_component, dashboard=self) + + # Return the three arguments to django.conf.urls.defaults.include + return urlpatterns, self.slug, self.slug + + def _autodiscover(self): + """ Discovers panels to register from the current dashboard module. """ + if getattr(self, "_autodiscover_complete", False): + return + + panels_to_discover = [] + panel_groups = [] + # If we have a flat iterable of panel names, wrap it again so + # we have a consistent structure for the next step. + if all([isinstance(i, basestring) for i in self.panels]): + self.panels = [self.panels] + + # Now iterate our panel sets. + for panel_set in self.panels: + # Instantiate PanelGroup classes. + if not isinstance(panel_set, collections.Iterable) and \ + issubclass(panel_set, PanelGroup): + panel_group = panel_set(self) + # Check for nested tuples, and convert them to PanelGroups + elif not isinstance(panel_set, PanelGroup): + panel_group = PanelGroup(self, panels=panel_set) + + # Put our results into their appropriate places + panels_to_discover.extend(panel_group.panels) + panel_groups.append((panel_group.slug, panel_group)) + + self._panel_groups = SortedDict(panel_groups) + + # Do the actual discovery + package = '.'.join(self.__module__.split('.')[:-1]) + mod = import_module(package) + for panel in panels_to_discover: + try: + before_import_registry = copy.copy(self._registry) + import_module('.%s.panel' % panel, package) + except: + self._registry = before_import_registry + if module_has_submodule(mod, panel): + raise + self._autodiscover_complete = True + + @classmethod + def register(cls, panel): + """ Registers a :class:`~horizon.Panel` with this dashboard. """ + panel_class = Horizon.register_panel(cls, panel) + # Support template loading from panel template directories. + panel_mod = import_module(panel.__module__) + panel_dir = os.path.dirname(panel_mod.__file__) + template_dir = os.path.join(panel_dir, "templates") + if os.path.exists(template_dir): + key = os.path.join(cls.slug, panel.slug) + loaders.panel_template_dirs[key] = template_dir + return panel_class + + @classmethod + def unregister(cls, panel): + """ Unregisters a :class:`~horizon.Panel` from this dashboard. """ + success = Horizon.unregister_panel(cls, panel) + if success: + # Remove the panel's template directory. + key = os.path.join(cls.slug, panel.slug) + if key in loaders.panel_template_dirs: + del loaders.panel_template_dirs[key] + return success + + +class Workflow(object): + def __init__(*args, **kwargs): + raise NotImplementedError() + + +try: + from django.utils.functional import empty +except ImportError: + #Django 1.3 fallback + empty = None + + +class LazyURLPattern(SimpleLazyObject): + def __iter__(self): + if self._wrapped is empty: + self._setup() + return iter(self._wrapped) + + def __reversed__(self): + if self._wrapped is empty: + self._setup() + return reversed(self._wrapped) + + +class Site(Registry, HorizonComponent): + """ The overarching class which encompasses all dashboards and panels. """ + + # Required for registry + _registerable_class = Dashboard + + name = "Horizon" + namespace = 'horizon' + slug = 'horizon' + urls = 'horizon.site_urls' + + def __repr__(self): + return u"" % self.slug + + @property + def _conf(self): + return conf.HORIZON_CONFIG + + @property + def dashboards(self): + return self._conf['dashboards'] + + @property + def default_dashboard(self): + return self._conf['default_dashboard'] + + def register(self, dashboard): + """ Registers a :class:`~horizon.Dashboard` with Horizon.""" + return self._register(dashboard) + + def unregister(self, dashboard): + """ Unregisters a :class:`~horizon.Dashboard` from Horizon. """ + return self._unregister(dashboard) + + def registered(self, dashboard): + return self._registered(dashboard) + + def register_panel(self, dashboard, panel): + dash_instance = self.registered(dashboard) + return dash_instance._register(panel) + + def unregister_panel(self, dashboard, panel): + dash_instance = self.registered(dashboard) + if not dash_instance: + raise NotRegistered("The dashboard %s is not registered." + % dashboard) + return dash_instance._unregister(panel) + + def get_dashboard(self, dashboard): + """ Returns the specified :class:`~horizon.Dashboard` instance. """ + return self._registered(dashboard) + + def get_dashboards(self): + """ Returns an ordered tuple of :class:`~horizon.Dashboard` modules. + + Orders dashboards according to the ``"dashboards"`` key in + ``HORIZON_CONFIG`` or else returns all registered dashboards + in alphabetical order. + + Any remaining :class:`~horizon.Dashboard` classes registered with + Horizon but not listed in ``HORIZON_CONFIG['dashboards']`` + will be appended to the end of the list alphabetically. + """ + if self.dashboards: + registered = copy.copy(self._registry) + dashboards = [] + for item in self.dashboards: + dashboard = self._registered(item) + dashboards.append(dashboard) + registered.pop(dashboard.__class__) + if len(registered): + extra = registered.values() + extra.sort() + dashboards.extend(extra) + return dashboards + else: + dashboards = self._registry.values() + dashboards.sort() + return dashboards + + def get_default_dashboard(self): + """ Returns the default :class:`~horizon.Dashboard` instance. + + If ``"default_dashboard"`` is specified in ``HORIZON_CONFIG`` + then that dashboard will be returned. If not, the first dashboard + returned by :func:`~horizon.get_dashboards` will be returned. + """ + if self.default_dashboard: + return self._registered(self.default_dashboard) + elif len(self._registry): + return self.get_dashboards()[0] + else: + raise NotRegistered("No dashboard modules have been registered.") + + def get_user_home(self, user): + """ Returns the default URL for a particular user. + + This method can be used to customize where a user is sent when + they log in, etc. By default it returns the value of + :meth:`get_absolute_url`. + + An alternative function can be supplied to customize this behavior + by specifying a either a URL or a function which returns a URL via + the ``"user_home"`` key in ``HORIZON_CONFIG``. Each of these + would be valid:: + + {"user_home": "/home",} # A URL + {"user_home": "my_module.get_user_home",} # Path to a function + {"user_home": lambda user: "/" + user.name,} # A function + {"user_home": None,} # Will always return the default dashboard + + This can be useful if the default dashboard may not be accessible + to all users. When user_home is missing from HORIZON_CONFIG, + it will default to the settings.LOGIN_REDIRECT_URL value. + """ + user_home = self._conf['user_home'] + if user_home: + if callable(user_home): + return user_home(user) + elif isinstance(user_home, basestring): + # Assume we've got a URL if there's a slash in it + if user_home.find("/") != -1: + return user_home + else: + mod, func = user_home.rsplit(".", 1) + return getattr(import_module(mod), func)(user) + # If it's not callable and not a string, it's wrong. + raise ValueError('The user_home setting must be either a string ' + 'or a callable object (e.g. a function).') + else: + return self.get_absolute_url() + + def get_absolute_url(self): + """ Returns the default URL for Horizon's URLconf. + + The default URL is determined by calling + :meth:`~horizon.Dashboard.get_absolute_url` + on the :class:`~horizon.Dashboard` instance returned by + :meth:`~horizon.get_default_dashboard`. + """ + return self.get_default_dashboard().get_absolute_url() + + @property + def _lazy_urls(self): + """ Lazy loading for URL patterns. + + This method avoids problems associated with attempting to evaluate + the the URLconf before the settings module has been loaded. + """ + def url_patterns(): + return self._urls()[0] + + return LazyURLPattern(url_patterns), self.namespace, self.slug + + def _urls(self): + """ Constructs the URLconf for Horizon from registered Dashboards. """ + urlpatterns = self._get_default_urlpatterns() + self._autodiscover() + + # Discover each dashboard's panels. + for dash in self._registry.values(): + dash._autodiscover() + + # Allow for override modules + if self._conf.get("customization_module", None): + customization_module = self._conf["customization_module"] + bits = customization_module.split('.') + mod_name = bits.pop() + package = '.'.join(bits) + mod = import_module(package) + try: + before_import_registry = copy.copy(self._registry) + import_module('%s.%s' % (package, mod_name)) + except: + self._registry = before_import_registry + if module_has_submodule(mod, mod_name): + raise + + # Compile the dynamic urlconf. + for dash in self._registry.values(): + urlpatterns += patterns('', + url(r'^%s/' % dash.slug, include(dash._decorated_urls))) + + # Return the three arguments to django.conf.urls.defaults.include + return urlpatterns, self.namespace, self.slug + + def _autodiscover(self): + """ Discovers modules to register from ``settings.INSTALLED_APPS``. + + This makes sure that the appropriate modules get imported to register + themselves with Horizon. + """ + if not getattr(self, '_registerable_class', None): + raise ImproperlyConfigured('You must set a ' + '"_registerable_class" property ' + 'in order to use autodiscovery.') + # Discover both dashboards and panels, in that order + for mod_name in ('dashboard', 'panel'): + for app in settings.INSTALLED_APPS: + mod = import_module(app) + try: + before_import_registry = copy.copy(self._registry) + import_module('%s.%s' % (app, mod_name)) + except: + self._registry = before_import_registry + if module_has_submodule(mod, mod_name): + raise + + +class HorizonSite(Site): + """ + A singleton implementation of Site such that all dealings with horizon + get the same instance no matter what. There can be only one. + """ + _instance = None + + def __new__(cls, *args, **kwargs): + if not cls._instance: + cls._instance = super(Site, cls).__new__(cls, *args, **kwargs) + return cls._instance + + +# The one true Horizon +Horizon = HorizonSite() diff --git a/dashboard/horizon/browsers/__init__.py b/dashboard/horizon/browsers/__init__.py new file mode 100644 index 00000000..c4fee973 --- /dev/null +++ b/dashboard/horizon/browsers/__init__.py @@ -0,0 +1,18 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 .base import ResourceBrowser +from .views import ResourceBrowserView diff --git a/dashboard/horizon/browsers/base.py b/dashboard/horizon/browsers/base.py new file mode 100644 index 00000000..30a7b151 --- /dev/null +++ b/dashboard/horizon/browsers/base.py @@ -0,0 +1,150 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 django import template +from django.utils.translation import ugettext_lazy as _ + +from horizon.tables import DataTable +from horizon.utils import html +from .breadcrumb import Breadcrumb + + +class ResourceBrowser(html.HTMLElement): + """A class which defines a browser for displaying data. + + .. attribute:: name + + A short name or slug for the browser. + + .. attribute:: verbose_name + + A more verbose name for the browser meant for display purposes. + + .. attribute:: navigation_table_class + + This table displays data on the left side of the browser. + Set the ``navigation_table_class`` attribute with + the desired :class:`~horizon.tables.DataTable` class. + This table class must set browser_table attribute in Meta to + ``"navigation"``. + + .. attribute:: content_table_class + + This table displays data on the right side of the browser. + Set the ``content_table_class`` attribute with + the desired :class:`~horizon.tables.DataTable` class. + This table class must set browser_table attribute in Meta to + ``"content"``. + + .. attribute:: navigation_kwarg_name + + This attribute represents the key of the navigatable items in the + kwargs property of this browser's view. + Defaults to ``"navigation_kwarg"``. + + .. attribute:: content_kwarg_name + + This attribute represents the key of the content items in the + kwargs property of this browser's view. + Defaults to ``"content_kwarg"``. + + .. attribute:: template + + String containing the template which should be used to render + the browser. Defaults to ``"horizon/common/_resource_browser.html"``. + + .. attribute:: context_var_name + + The name of the context variable which will contain the browser when + it is rendered. Defaults to ``"browser"``. + + .. attribute:: has_breadcrumb + + Indicates if the content table of the browser would have breadcrumb. + Defaults to false. + + .. attribute:: breadcrumb_template + + This is a template used to render the breadcrumb. + Defaults to ``"horizon/common/_breadcrumb.html"``. + """ + name = None + verbose_name = None + navigation_table_class = None + content_table_class = None + navigation_kwarg_name = "navigation_kwarg" + content_kwarg_name = "content_kwarg" + navigable_item_name = _("Navigation Item") + template = "horizon/common/_resource_browser.html" + context_var_name = "browser" + has_breadcrumb = False + breadcrumb_template = "horizon/common/_breadcrumb.html" + breadcrumb_url = None + + def __init__(self, request, tables_dict=None, attrs=None, **kwargs): + super(ResourceBrowser, self).__init__() + self.name = self.name or self.__class__.__name__ + self.verbose_name = self.verbose_name or self.name.title() + self.request = request + self.kwargs = kwargs + self.has_breadcrumb = getattr(self, "has_breadcrumb") + if self.has_breadcrumb: + self.breadcrumb_template = getattr(self, "breadcrumb_template") + self.breadcrumb_url = getattr(self, "breadcrumb_url") + if not self.breadcrumb_url: + raise ValueError("You must specify a breadcrumb_url " + "if the has_breadcrumb is set to True.") + self.attrs.update(attrs or {}) + self.check_table_class(self.content_table_class, "content_table_class") + self.check_table_class(self.navigation_table_class, + "navigation_table_class") + if tables_dict: + self.set_tables(tables_dict) + + def check_table_class(self, cls, attr_name): + if not cls or not issubclass(cls, DataTable): + raise ValueError("You must specify a DataTable subclass for " + "the %s attribute on %s." + % (attr_name, self.__class__.__name__)) + + def set_tables(self, tables): + """ + Sets the table instances on the browser from a dictionary mapping table + names to table instances (as constructed by MultiTableView). + """ + self.navigation_table = tables[self.navigation_table_class._meta.name] + self.content_table = tables[self.content_table_class._meta.name] + navigation_item = self.kwargs.get(self.navigation_kwarg_name) + content_path = self.kwargs.get(self.content_kwarg_name) + # Tells the navigation table what is selected. + self.navigation_table.current_item_id = navigation_item + if self.has_breadcrumb: + self.prepare_breadcrumb(tables, navigation_item, content_path) + + def prepare_breadcrumb(self, tables, navigation_item, content_path): + if self.has_breadcrumb and navigation_item and content_path: + for table in tables.values(): + table.breadcrumb = Breadcrumb(self.request, + self.breadcrumb_template, + navigation_item, + content_path, + self.breadcrumb_url) + + def render(self): + browser_template = template.loader.get_template(self.template) + extra_context = {self.context_var_name: self} + context = template.RequestContext(self.request, extra_context) + return browser_template.render(context) diff --git a/dashboard/horizon/browsers/breadcrumb.py b/dashboard/horizon/browsers/breadcrumb.py new file mode 100644 index 00000000..ba1ca748 --- /dev/null +++ b/dashboard/horizon/browsers/breadcrumb.py @@ -0,0 +1,48 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 django import template + +from horizon.utils import html + + +class Breadcrumb(html.HTMLElement): + def __init__(self, request, template, root, + subfolder_path, url, attr=None): + super(Breadcrumb, self).__init__() + self.template = template + self.request = request + self.root = root + self.subfolder_path = subfolder_path + self.url = url + self._subfolders = [] + + def get_subfolders(self): + if self.subfolder_path and not self._subfolders: + (parent, slash, folder) = self.subfolder_path.strip('/') \ + .rpartition('/') + while folder: + path = "%s%s%s/" % (parent, slash, folder) + self._subfolders.insert(0, (folder, path)) + (parent, slash, folder) = parent.rpartition('/') + return self._subfolders + + def render(self): + """ Renders the table using the template from the table options. """ + breadcrumb_template = template.loader.get_template(self.template) + extra_context = {"breadcrumb": self} + context = template.RequestContext(self.request, extra_context) + return breadcrumb_template.render(context) diff --git a/dashboard/horizon/browsers/views.py b/dashboard/horizon/browsers/views.py new file mode 100644 index 00000000..c2e8b437 --- /dev/null +++ b/dashboard/horizon/browsers/views.py @@ -0,0 +1,49 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 django.utils.translation import ugettext_lazy as _ + +from horizon.tables import MultiTableView + + +class ResourceBrowserView(MultiTableView): + browser_class = None + + def __init__(self, *args, **kwargs): + if not self.browser_class: + raise ValueError("You must specify a ResourceBrowser subclass " + "for the browser_class attribute on %s." + % self.__class__.__name__) + self.table_classes = (self.browser_class.navigation_table_class, + self.browser_class.content_table_class) + self.navigation_selection = False + super(ResourceBrowserView, self).__init__(*args, **kwargs) + + def get_browser(self): + if not hasattr(self, "browser"): + self.browser = self.browser_class(self.request, **self.kwargs) + self.browser.set_tables(self.get_tables()) + if not self.navigation_selection: + ct = self.browser.content_table + item = self.browser.navigable_item_name.lower() + ct._no_data_message = _("Select a %s to browse.") % item + return self.browser + + def get_context_data(self, **kwargs): + context = super(ResourceBrowserView, self).get_context_data(**kwargs) + browser = self.get_browser() + context["%s_browser" % browser.name] = browser + return context diff --git a/dashboard/horizon/conf/__init__.py b/dashboard/horizon/conf/__init__.py new file mode 100644 index 00000000..b34b4f51 --- /dev/null +++ b/dashboard/horizon/conf/__init__.py @@ -0,0 +1,34 @@ +import copy + +from django.utils.functional import LazyObject, empty + + +class LazySettings(LazyObject): + def _setup(self, name=None): + from django.conf import settings + from .default import HORIZON_CONFIG as DEFAULT_CONFIG + HORIZON_CONFIG = copy.copy(DEFAULT_CONFIG) + HORIZON_CONFIG.update(settings.HORIZON_CONFIG) + + # Ensure we always have our exception configuration... + for exc_category in ['unauthorized', 'not_found', 'recoverable']: + if exc_category not in HORIZON_CONFIG['exceptions']: + default_exc_config = DEFAULT_CONFIG['exceptions'][exc_category] + HORIZON_CONFIG['exceptions'][exc_category] = default_exc_config + + # Ensure our password validator always exists... + if 'regex' not in HORIZON_CONFIG['password_validator']: + default_pw_regex = DEFAULT_CONFIG['password_validator']['regex'] + HORIZON_CONFIG['password_validator']['regex'] = default_pw_regex + if 'help_text' not in HORIZON_CONFIG['password_validator']: + default_pw_help = DEFAULT_CONFIG['password_validator']['help_text'] + HORIZON_CONFIG['password_validator']['help_text'] = default_pw_help + + self._wrapped = HORIZON_CONFIG + + def __getitem__(self, name, fallback=None): + if self._wrapped is empty: + self._setup(name) + return self._wrapped.get(name, fallback) + +HORIZON_CONFIG = LazySettings() diff --git a/dashboard/horizon/conf/dash_template/__init__.py b/dashboard/horizon/conf/dash_template/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashboard/horizon/conf/dash_template/dashboard.py.tmpl b/dashboard/horizon/conf/dash_template/dashboard.py.tmpl new file mode 100644 index 00000000..9e435bef --- /dev/null +++ b/dashboard/horizon/conf/dash_template/dashboard.py.tmpl @@ -0,0 +1,13 @@ +from django.utils.translation import ugettext_lazy as _ + +import horizon + + +class {{ dash_name|title }}(horizon.Dashboard): + name = _("{{ dash_name|title }}") + slug = "{{ dash_name|slugify }}" + panels = () # Add your panels here. + default_panel = '' # Specify the slug of the dashboard's default panel. + + +horizon.register({{ dash_name|title }}) diff --git a/dashboard/horizon/conf/dash_template/models.py b/dashboard/horizon/conf/dash_template/models.py new file mode 100644 index 00000000..1b3d5f9e --- /dev/null +++ b/dashboard/horizon/conf/dash_template/models.py @@ -0,0 +1,3 @@ +""" +Stub file to work around django bug: https://code.djangoproject.com/ticket/7198 +""" diff --git a/dashboard/horizon/conf/dash_template/static/dash_name/css/dash_name.css b/dashboard/horizon/conf/dash_template/static/dash_name/css/dash_name.css new file mode 100644 index 00000000..ed03b4f6 --- /dev/null +++ b/dashboard/horizon/conf/dash_template/static/dash_name/css/dash_name.css @@ -0,0 +1 @@ +/* Additional CSS for {{ dash_name }}. */ diff --git a/dashboard/horizon/conf/dash_template/static/dash_name/js/dash_name.js b/dashboard/horizon/conf/dash_template/static/dash_name/js/dash_name.js new file mode 100644 index 00000000..a8088523 --- /dev/null +++ b/dashboard/horizon/conf/dash_template/static/dash_name/js/dash_name.js @@ -0,0 +1 @@ +/* Additional JavaScript for {{ dash_name }}. */ diff --git a/dashboard/horizon/conf/dash_template/templates/dash_name/base.html b/dashboard/horizon/conf/dash_template/templates/dash_name/base.html new file mode 100644 index 00000000..f07a01ba --- /dev/null +++ b/dashboard/horizon/conf/dash_template/templates/dash_name/base.html @@ -0,0 +1,11 @@ +{% load horizon %}{% jstemplate %}[% extends 'base.html' %] + +[% block sidebar %] + [% include 'horizon/common/_sidebar.html' %] +[% endblock %] + +[% block main %] + [% include "horizon/_messages.html" %] + [% block {{ dash_name }}_main %][% endblock %] +[% endblock %] +{% endjstemplate %} diff --git a/dashboard/horizon/conf/default.py b/dashboard/horizon/conf/default.py new file mode 100644 index 00000000..376a20c6 --- /dev/null +++ b/dashboard/horizon/conf/default.py @@ -0,0 +1,35 @@ +from django.conf import settings +from django.utils.translation import ugettext as _ + +# Default configuration dictionary. Do not mutate. +HORIZON_CONFIG = { + # Allow for ordering dashboards; list or tuple if provided. + 'dashboards': None, + + # Name of a default dashboard; defaults to first alphabetically if None + 'default_dashboard': None, + + # Default redirect url for users' home + 'user_home': settings.LOGIN_REDIRECT_URL, + + # AJAX settings for JavaScript + 'ajax_queue_limit': 10, + 'ajax_poll_interval': 2500, + + # URL for additional help with this site. + 'help_url': None, + + # Exception configuration. + 'exceptions': {'unauthorized': [], + 'not_found': [], + 'recoverable': []}, + + # Password configuration. + 'password_validator': {'regex': '.*', + 'help_text': _("Password is not accepted")}, + + 'password_autocomplete': 'on', + + # Enable or disable simplified floating IP address management. + 'simple_ip_management': True +} diff --git a/dashboard/horizon/conf/panel_template/__init__.py b/dashboard/horizon/conf/panel_template/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashboard/horizon/conf/panel_template/models.py b/dashboard/horizon/conf/panel_template/models.py new file mode 100644 index 00000000..1b3d5f9e --- /dev/null +++ b/dashboard/horizon/conf/panel_template/models.py @@ -0,0 +1,3 @@ +""" +Stub file to work around django bug: https://code.djangoproject.com/ticket/7198 +""" diff --git a/dashboard/horizon/conf/panel_template/panel.py.tmpl b/dashboard/horizon/conf/panel_template/panel.py.tmpl new file mode 100644 index 00000000..4364438e --- /dev/null +++ b/dashboard/horizon/conf/panel_template/panel.py.tmpl @@ -0,0 +1,13 @@ +from django.utils.translation import ugettext_lazy as _ + +import horizon + +from {{ dash_path }} import dashboard + + +class {{ panel_name|title }}(horizon.Panel): + name = _("{{ panel_name|title }}") + slug = "{{ panel_name|slugify }}" + + +dashboard.{{ dash_name|title }}.register({{ panel_name|title }}) diff --git a/dashboard/horizon/conf/panel_template/templates/panel_name/index.html b/dashboard/horizon/conf/panel_template/templates/panel_name/index.html new file mode 100644 index 00000000..5185396b --- /dev/null +++ b/dashboard/horizon/conf/panel_template/templates/panel_name/index.html @@ -0,0 +1,12 @@ +{% load horizon %}{% jstemplate %}[% extends '{{ dash_name }}/base.html' %] +[% load i18n %] +[% block title %][% trans "{{ panel_name|title }}" %][% endblock %] + +[% block page_header %] + [% include "horizon/common/_page_header.html" with title=_("{{ panel_name|title }}") %] +[% endblock page_header %] + +[% block {{ dash_name }}_main %] +[% endblock %] + +{% endjstemplate %} diff --git a/dashboard/horizon/conf/panel_template/tests.py.tmpl b/dashboard/horizon/conf/panel_template/tests.py.tmpl new file mode 100644 index 00000000..e8bba98c --- /dev/null +++ b/dashboard/horizon/conf/panel_template/tests.py.tmpl @@ -0,0 +1,7 @@ +from horizon.test import helpers as test + + +class {{ panel_name|title}}Tests(test.TestCase): + # Unit tests for {{ panel_name }}. + def test_me(self): + self.assertTrue(1 + 1 == 2) diff --git a/dashboard/horizon/conf/panel_template/urls.py b/dashboard/horizon/conf/panel_template/urls.py new file mode 100644 index 00000000..b549cca5 --- /dev/null +++ b/dashboard/horizon/conf/panel_template/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls.defaults import patterns, url + +from .views import IndexView + + +urlpatterns = patterns('', + url(r'^$', IndexView.as_view(), name='index'), +) diff --git a/dashboard/horizon/conf/panel_template/views.py b/dashboard/horizon/conf/panel_template/views.py new file mode 100644 index 00000000..a5116ca4 --- /dev/null +++ b/dashboard/horizon/conf/panel_template/views.py @@ -0,0 +1,10 @@ +from horizon import views + + +class IndexView(views.APIView): + # A very simple class-based view... + template_name = '{{ dash_name }}/{{ panel_name }}/index.html' + + def get_data(self, request, context, *args, **kwargs): + # Add data to the context here... + return context diff --git a/dashboard/horizon/context_processors.py b/dashboard/horizon/context_processors.py new file mode 100644 index 00000000..f5d2ecfd --- /dev/null +++ b/dashboard/horizon/context_processors.py @@ -0,0 +1,44 @@ +# 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. +""" +Context processors used by Horizon. +""" + +from horizon import conf + + +def horizon(request): + """ The main Horizon context processor. Required for Horizon to function. + + It adds the Horizon config to the context as well as setting the names + ``True`` and ``False`` in the context to their boolean equivalents + for convenience. + + .. warning:: + + Don't put API calls in context processors; they will be called once + for each template/template fragment which takes context that is used + to render the complete output. + """ + context = {"HORIZON_CONFIG": conf.HORIZON_CONFIG, + "True": True, + "False": False} + + return context diff --git a/dashboard/horizon/decorators.py b/dashboard/horizon/decorators.py new file mode 100644 index 00000000..5ee67724 --- /dev/null +++ b/dashboard/horizon/decorators.py @@ -0,0 +1,94 @@ +# 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 CRS4 +# +# 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. + +""" +General-purpose decorators for use with Horizon. +""" +import functools + +from django.utils.decorators import available_attrs +from django.utils.translation import ugettext as _ + + +def _current_component(view_func, dashboard=None, panel=None): + """ Sets the currently-active dashboard and/or panel on the request. """ + @functools.wraps(view_func, assigned=available_attrs(view_func)) + def dec(request, *args, **kwargs): + if dashboard: + request.horizon['dashboard'] = dashboard + if panel: + request.horizon['panel'] = panel + return view_func(request, *args, **kwargs) + return dec + + +def require_auth(view_func): + """ Performs user authentication check. + + Similar to Django's `login_required` decorator, except that this throws + :exc:`~horizon.exceptions.NotAuthenticated` exception if the user is not + signed-in. + """ + from horizon.exceptions import NotAuthenticated + + @functools.wraps(view_func, assigned=available_attrs(view_func)) + def dec(request, *args, **kwargs): + if request.user.is_authenticated(): + return view_func(request, *args, **kwargs) + raise NotAuthenticated(_("Please log in to continue.")) + return dec + + +def require_perms(view_func, required): + """ Enforces permission-based access controls. + + :param list required: A tuple of permission names, all of which the request + user must possess in order access the decorated view. + + Example usage:: + + from horizon.decorators import require_perms + + + @require_perms(['foo.admin', 'foo.member']) + def my_view(request): + ... + + Raises a :exc:`~horizon.exceptions.NotAuthorized` exception if the + requirements are not met. + """ + from horizon.exceptions import NotAuthorized + # We only need to check each permission once for a view, so we'll use a set + current_perms = getattr(view_func, '_required_perms', set([])) + view_func._required_perms = current_perms | set(required) + + @functools.wraps(view_func, assigned=available_attrs(view_func)) + def dec(request, *args, **kwargs): + if request.user.is_authenticated(): + if request.user.has_perms(view_func._required_perms): + return view_func(request, *args, **kwargs) + raise NotAuthorized(_("You are not authorized to access %s") + % request.path) + + # If we don't have any permissions, just return the original view. + if required: + return dec + else: + return view_func diff --git a/dashboard/horizon/exceptions.py b/dashboard/horizon/exceptions.py new file mode 100644 index 00000000..5f17a4e8 --- /dev/null +++ b/dashboard/horizon/exceptions.py @@ -0,0 +1,314 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +""" +Exceptions raised by the Horizon code and the machinery for handling them. +""" + +import logging +import os +import sys + +from django.contrib.auth import logout +from django.http import HttpRequest +from django.utils import termcolors +from django.utils.translation import ugettext as _ +from django.views.debug import SafeExceptionReporterFilter, CLEANSED_SUBSTITUTE + +from horizon import messages +from horizon.conf import HORIZON_CONFIG + +LOG = logging.getLogger(__name__) +PALETTE = termcolors.PALETTES[termcolors.DEFAULT_PALETTE] + + +class HorizonReporterFilter(SafeExceptionReporterFilter): + """ Error report filter that's always active, even in DEBUG mode. """ + def is_active(self, request): + return True + + # TODO(gabriel): This bugfix is cribbed from Django's code. When 1.4.1 + # is available we can remove this code. + def get_traceback_frame_variables(self, request, tb_frame): + """ + Replaces the values of variables marked as sensitive with + stars (*********). + """ + # Loop through the frame's callers to see if the sensitive_variables + # decorator was used. + current_frame = tb_frame.f_back + sensitive_variables = None + while current_frame is not None: + if (current_frame.f_code.co_name == 'sensitive_variables_wrapper' + and 'sensitive_variables_wrapper' + in current_frame.f_locals): + # The sensitive_variables decorator was used, so we take note + # of the sensitive variables' names. + wrapper = current_frame.f_locals['sensitive_variables_wrapper'] + sensitive_variables = getattr(wrapper, + 'sensitive_variables', + None) + break + current_frame = current_frame.f_back + + cleansed = [] + if self.is_active(request) and sensitive_variables: + if sensitive_variables == '__ALL__': + # Cleanse all variables + for name, value in tb_frame.f_locals.items(): + cleansed.append((name, CLEANSED_SUBSTITUTE)) + return cleansed + else: + # Cleanse specified variables + for name, value in tb_frame.f_locals.items(): + if name in sensitive_variables: + value = CLEANSED_SUBSTITUTE + elif isinstance(value, HttpRequest): + # Cleanse the request's POST parameters. + value = self.get_request_repr(value) + cleansed.append((name, value)) + return cleansed + else: + # Potentially cleanse only the request if it's one of the + # frame variables. + for name, value in tb_frame.f_locals.items(): + if isinstance(value, HttpRequest): + # Cleanse the request's POST parameters. + value = self.get_request_repr(value) + cleansed.append((name, value)) + return cleansed + + +class HorizonException(Exception): + """ Base exception class for distinguishing our own exception classes. """ + pass + + +class Http302(HorizonException): + """ + Error class which can be raised from within a handler to cause an + early bailout and redirect at the middleware level. + """ + status_code = 302 + + def __init__(self, location, message=None): + self.location = location + self.message = message + + +class NotAuthorized(HorizonException): + """ + Raised whenever a user attempts to access a resource which they do not + have permission-based access to (such as when failing the + :func:`~horizon.decorators.require_perms` decorator). + + The included :class:`~horizon.middleware.HorizonMiddleware` catches + ``NotAuthorized`` and handles it gracefully by displaying an error + message and redirecting the user to a login page. + """ + status_code = 401 + + +class NotAuthenticated(HorizonException): + """ + Raised when a user is trying to make requests and they are not logged in. + + The included :class:`~horizon.middleware.HorizonMiddleware` catches + ``NotAuthenticated`` and handles it gracefully by displaying an error + message and redirecting the user to a login page. + """ + status_code = 403 + + +class NotFound(HorizonException): + """ Generic error to replace all "Not Found"-type API errors. """ + status_code = 404 + + +class RecoverableError(HorizonException): + """ Generic error to replace any "Recoverable"-type API errors. """ + status_code = 100 # HTTP status code "Continue" + + +class ServiceCatalogException(HorizonException): + """ + Raised when a requested service is not available in the ``ServiceCatalog`` + returned by Keystone. + """ + def __init__(self, service_name): + message = 'Invalid service catalog service: %s' % service_name + super(ServiceCatalogException, self).__init__(message) + + +class AlreadyExists(HorizonException): + """ + Exception to be raised when trying to create an API resource which + already exists. + """ + def __init__(self, name, resource_type): + self.attrs = {"name": name, "resource": resource_type} + self.msg = 'A %(resource)s with the name "%(name)s" already exists.' + + def __repr__(self): + return self.msg % self.attrs + + def __str__(self): + return self.msg % self.attrs + + def __unicode__(self): + return _(self.msg) % self.attrs + + +class WorkflowError(HorizonException): + """ Exception to be raised when something goes wrong in a workflow. """ + pass + + +class WorkflowValidationError(HorizonException): + """ + Exception raised during workflow validation if required data is missing, + or existing data is not valid. + """ + pass + + +class HandledException(HorizonException): + """ + Used internally to track exceptions that have gone through + :func:`horizon.exceptions.handle` more than once. + """ + def __init__(self, wrapped): + self.wrapped = wrapped + + +UNAUTHORIZED = tuple(HORIZON_CONFIG['exceptions']['unauthorized']) +NOT_FOUND = tuple(HORIZON_CONFIG['exceptions']['not_found']) +RECOVERABLE = (AlreadyExists,) +RECOVERABLE += tuple(HORIZON_CONFIG['exceptions']['recoverable']) + + +def error_color(msg): + return termcolors.colorize(msg, **PALETTE['ERROR']) + + +def check_message(keywords, message): + """ + Checks an exception for given keywords and raises a new ``ActionError`` + with the desired message if the keywords are found. This allows selective + control over API error messages. + """ + exc_type, exc_value, exc_traceback = sys.exc_info() + if set(str(exc_value).split(" ")).issuperset(set(keywords)): + exc_value._safe_message = message + raise + + +def handle(request, message=None, redirect=None, ignore=False, + escalate=False, log_level=None, force_log=None): + """ Centralized error handling for Horizon. + + Because Horizon consumes so many different APIs with completely + different ``Exception`` types, it's necessary to have a centralized + place for handling exceptions which may be raised. + + Exceptions are roughly divided into 3 types: + + #. ``UNAUTHORIZED``: Errors resulting from authentication or authorization + problems. These result in being logged out and sent to the login screen. + #. ``NOT_FOUND``: Errors resulting from objects which could not be + located via the API. These generally result in a user-facing error + message, but are otherwise returned to the normal code flow. Optionally + a redirect value may be passed to the error handler so users are + returned to a different view than the one requested in addition to the + error message. + #. RECOVERABLE: Generic API errors which generate a user-facing message + but drop directly back to the regular code flow. + + All other exceptions bubble the stack as normal unless the ``ignore`` + argument is passed in as ``True``, in which case only unrecognized + errors are bubbled. + + If the exception is not re-raised, an appropriate wrapper exception + class indicating the type of exception that was encountered will be + returned. + """ + exc_type, exc_value, exc_traceback = sys.exc_info() + log_method = getattr(LOG, log_level or "exception") + force_log = force_log or os.environ.get("HORIZON_TEST_RUN", False) + force_silence = getattr(exc_value, "silence_logging", False) + + # Because the same exception may travel through this method more than + # once (if it's re-raised) we may want to treat it differently + # the second time (e.g. no user messages/logging). + handled = issubclass(exc_type, HandledException) + wrap = False + + # Restore our original exception information, but re-wrap it at the end + if handled: + exc_type, exc_value, exc_traceback = exc_value.wrapped + wrap = True + + # We trust messages from our own exceptions + if issubclass(exc_type, HorizonException): + message = exc_value + # Check for an override message + elif getattr(exc_value, "_safe_message", None): + message = exc_value._safe_message + # If the message has a placeholder for the exception, fill it in + elif message and "%(exc)s" in message: + message = message % {"exc": exc_value} + + if issubclass(exc_type, UNAUTHORIZED): + if ignore: + return NotAuthorized + logout(request) + if not force_silence and not handled: + log_method(error_color("Unauthorized: %s" % exc_value)) + if not handled: + # We get some pretty useless error messages back from + # some clients, so let's define our own fallback. + fallback = _("Unauthorized. Please try logging in again.") + messages.error(request, message or fallback, extra_tags="login") + raise NotAuthorized # Redirect handled in middleware + + if issubclass(exc_type, NOT_FOUND): + wrap = True + if not force_silence and not handled and (not ignore or force_log): + log_method(error_color("Not Found: %s" % exc_value)) + if not ignore and not handled: + messages.error(request, message or exc_value) + if redirect: + raise Http302(redirect) + if not escalate: + return NotFound # return to normal code flow + + if issubclass(exc_type, RECOVERABLE): + wrap = True + if not force_silence and not handled and (not ignore or force_log): + # Default recoverable error to WARN log level + log_method = getattr(LOG, log_level or "warning") + log_method(error_color("Recoverable error: %s" % exc_value)) + if not ignore and not handled: + messages.error(request, message or exc_value) + if redirect: + raise Http302(redirect) + if not escalate: + return RecoverableError # return to normal code flow + + # If we've gotten here, time to wrap and/or raise our exception. + if wrap: + raise HandledException([exc_type, exc_value, exc_traceback]) + raise exc_type, exc_value, exc_traceback diff --git a/dashboard/horizon/forms/__init__.py b/dashboard/horizon/forms/__init__.py new file mode 100644 index 00000000..e2844f5a --- /dev/null +++ b/dashboard/horizon/forms/__init__.py @@ -0,0 +1,24 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +# FIXME(gabriel): Legacy imports for API compatibility. +from django.forms import * +from django.forms import widgets + +# Convenience imports for public API components. +from .base import SelfHandlingMixin, SelfHandlingForm, DateForm +from .views import ModalFormView, ModalFormMixin +from .fields import DynamicTypedChoiceField, DynamicChoiceField diff --git a/dashboard/horizon/forms/base.py b/dashboard/horizon/forms/base.py new file mode 100644 index 00000000..2357ea50 --- /dev/null +++ b/dashboard/horizon/forms/base.py @@ -0,0 +1,60 @@ +# 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 django import forms +from django.forms.forms import NON_FIELD_ERRORS +from django.utils import dates, timezone + + +class SelfHandlingMixin(object): + def __init__(self, request, *args, **kwargs): + self.request = request + if not hasattr(self, "handle"): + raise NotImplementedError("%s does not define a handle method." + % self.__class__.__name__) + super(SelfHandlingMixin, self).__init__(*args, **kwargs) + + +class SelfHandlingForm(SelfHandlingMixin, forms.Form): + """ + A base :class:`Form ` class which includes + processing logic in its subclasses. + """ + def api_error(self, message): + """ + Adds an error to the form's error dictionary after validation + based on problems reported via the API. This is useful when you + wish for API errors to appear as errors on the form rather than + using the messages framework. + """ + self._errors[NON_FIELD_ERRORS] = self.error_class([message]) + + +class DateForm(forms.Form): + """ A simple form for selecting a start date. """ + month = forms.ChoiceField(choices=dates.MONTHS.items()) + year = forms.ChoiceField() + + def __init__(self, *args, **kwargs): + super(DateForm, self).__init__(*args, **kwargs) + years = [(year, year) for year + in xrange(2009, timezone.now().year + 1)] + years.reverse() + self.fields['year'].choices = years diff --git a/dashboard/horizon/forms/fields.py b/dashboard/horizon/forms/fields.py new file mode 100644 index 00000000..b728e74e --- /dev/null +++ b/dashboard/horizon/forms/fields.py @@ -0,0 +1,70 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 django.core import urlresolvers +from django.forms import fields, widgets + + +class DynamicSelectWidget(widgets.Select): + """ + A subclass of the ``Select`` widget which renders extra attributes for use + in callbacks to handle dynamic changes to the available choices. + """ + _data_add_url_attr = "data-add-item-url" + + def render(self, *args, **kwargs): + add_item_url = self.get_add_item_url() + if add_item_url is not None: + self.attrs.update({self._data_add_url_attr: add_item_url}) + return super(DynamicSelectWidget, self).render(*args, **kwargs) + + def get_add_item_url(self): + if callable(self.add_item_link): + return self.add_item_link() + try: + if self.add_item_link_args: + return urlresolvers.reverse(self.add_item_link, + args=[self.add_item_link_args]) + else: + return urlresolvers.reverse(self.add_item_link) + except urlresolvers.NoReverseMatch: + return self.add_item_link + + +class DynamicChoiceField(fields.ChoiceField): + """ + A subclass of ``ChoiceField`` with additional properties that make + dynamically updating its elements easier. + + Notably, the field declaration takes an extra argument, ``add_item_link`` + which may be a string or callable defining the URL that should be used + for the "add" link associated with the field. + """ + widget = DynamicSelectWidget + + def __init__(self, + add_item_link=None, + add_item_link_args=None, + *args, + **kwargs): + super(DynamicChoiceField, self).__init__(*args, **kwargs) + self.widget.add_item_link = add_item_link + self.widget.add_item_link_args = add_item_link_args + + +class DynamicTypedChoiceField(DynamicChoiceField, fields.TypedChoiceField): + """ Simple mix of ``DynamicChoiceField`` and ``TypedChoiceField``. """ + pass diff --git a/dashboard/horizon/forms/views.py b/dashboard/horizon/forms/views.py new file mode 100644 index 00000000..e942227a --- /dev/null +++ b/dashboard/horizon/forms/views.py @@ -0,0 +1,115 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 json +import os + +from django import http +from django.views import generic + +from horizon import exceptions + + +ADD_TO_FIELD_HEADER = "HTTP_X_HORIZON_ADD_TO_FIELD" + + +class ModalFormMixin(object): + def get_template_names(self): + if self.request.is_ajax(): + if not hasattr(self, "ajax_template_name"): + # Transform standard template name to ajax name (leading "_") + bits = list(os.path.split(self.template_name)) + bits[1] = "".join(("_", bits[1])) + self.ajax_template_name = os.path.join(*bits) + template = self.ajax_template_name + else: + template = self.template_name + return template + + def get_context_data(self, **kwargs): + context = super(ModalFormMixin, self).get_context_data(**kwargs) + if self.request.is_ajax(): + context['hide'] = True + if ADD_TO_FIELD_HEADER in self.request.META: + context['add_to_field'] = self.request.META[ADD_TO_FIELD_HEADER] + return context + + +class ModalFormView(ModalFormMixin, generic.FormView): + """ + The main view class from which all views which handle forms in Horizon + should inherit. It takes care of all details with processing + :class:`~horizon.forms.base.SelfHandlingForm` classes, and modal concerns + when the associated template inherits from + `horizon/common/_modal_form.html`. + + Subclasses must define a ``form_class`` and ``template_name`` attribute + at minimum. + + See Django's documentation on the `FormView `_ class for + more details. + """ + + def get_object_id(self, obj): + """ + For dynamic insertion of resources created in modals, this method + returns the id of the created object. Defaults to returning the ``id`` + attribute. + """ + return obj.id + + def get_object_display(self, obj): + """ + For dynamic insertion of resources created in modals, this method + returns the display name of the created object. Defaults to returning + the ``name`` attribute. + """ + return obj.name + + def get_form(self, form_class): + """ + Returns an instance of the form to be used in this view. + """ + return form_class(self.request, **self.get_form_kwargs()) + + def form_valid(self, form): + try: + handled = form.handle(self.request, form.cleaned_data) + except: + handled = None + exceptions.handle(self.request) + + if handled: + if ADD_TO_FIELD_HEADER in self.request.META: + field_id = self.request.META[ADD_TO_FIELD_HEADER] + data = [self.get_object_id(handled), + self.get_object_display(handled)] + response = http.HttpResponse(json.dumps(data)) + response["X-Horizon-Add-To-Field"] = field_id + else: + success_url = self.get_success_url() + response = http.HttpResponseRedirect(success_url) + # TODO(gabriel): This is not a long-term solution to how + # AJAX should be handled, but it's an expedient solution + # until the blueprint for AJAX handling is architected + # and implemented. + response['X-Horizon-Location'] = success_url + return response + else: + # If handled didn't return, we can assume something went + # wrong, and we should send back the form as-is. + return self.form_invalid(form) diff --git a/dashboard/horizon/loaders.py b/dashboard/horizon/loaders.py new file mode 100644 index 00000000..6ab86b8f --- /dev/null +++ b/dashboard/horizon/loaders.py @@ -0,0 +1,48 @@ +""" +Wrapper for loading templates from "templates" directories in panel modules. +""" + +import os + +from django.conf import settings +from django.template.base import TemplateDoesNotExist +from django.template.loader import BaseLoader +from django.utils._os import safe_join + +# Set up a cache of the panel directories to search. +panel_template_dirs = {} + + +class TemplateLoader(BaseLoader): + is_usable = True + + def get_template_sources(self, template_name): + bits = template_name.split(os.path.sep, 2) + if len(bits) == 3: + dash_name, panel_name, remainder = bits + key = os.path.join(dash_name, panel_name) + if key in panel_template_dirs: + template_dir = panel_template_dirs[key] + try: + yield safe_join(template_dir, panel_name, remainder) + except UnicodeDecodeError: + # The template dir name wasn't valid UTF-8. + raise + except ValueError: + # The joined path was located outside of template_dir. + pass + + def load_template_source(self, template_name, template_dirs=None): + for path in self.get_template_sources(template_name): + try: + file = open(path) + try: + return (file.read().decode(settings.FILE_CHARSET), path) + finally: + file.close() + except IOError: + pass + raise TemplateDoesNotExist(template_name) + + +_loader = TemplateLoader() diff --git a/dashboard/horizon/locale/bg_BG/LC_MESSAGES/django.mo b/dashboard/horizon/locale/bg_BG/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..d9a427c1f3b6aa9be464fd000b50c990de4f5b3c GIT binary patch literal 2950 zcmaKtU2GIp6vszJgeoYC--;K32&$EcqlOm0 zLX1_w=z~I{zM8Z^rM7f=@WmICJMqE97@myJCMG_jzjJ4xixO{k_BV6q+;jftoO^fA z-MHuzhp~q1F0O~KaGZx=@0ENpmR#jHcfh4^C0q-yft%nBunS%dcf)Jp3vekc!^JQx zSckIije>6%^B=%_m_JfHpM*>>eVNLwoFy7*ZGO8xqFqyA{e1%`#-WNuMQG8Isf8T%>P@)K2;+L+UGg zix${k-wUtjl0L~^P+X!__LU5&zl=M%q_@pOaOru- z}-$YDDG6w4nIh}*y#y}qd4}4Q?)0GtJ*^K^ebI1$B5$no(Ss;5xE)F0s4~q zQ0S$qs*_hQ&W(C;f=uz$?4oafA#GQrdi|=OI!~t~gznc#vNwv$%1>;%nljWV$ij1+SDvc;xnEGMeyZ9=h0wkza z9PPyy@;sI5SN*V}(h*X|6S6UnBWVQlrILMmg=NmOden2Cz2KsX^AU;iTHTBHD?d$C zJ<$~`@-9-V!BE%Jktp_G_sS(z$V)LsVnu=?Q&+T4#KD9*Jg?)BX;!`%)s;q^G=IT| zM3dSiiCULfn{s4LMo^||UR?DNP2I6uw?0p1L(<88H1tZcA(6Z!6St@AK9{$sQ|%Oe zQP{HEt3`3@_9Yd+>^@bmB<=wHZs~t|z}*^qS_rAdaNun+&vl4 zy?s@-aVd%`kGJ&duu`YQ?trJOovOEMSNC97_jdJIZ!e~o+H|U+%1cjmxAgV&ZC^;X zt<>7GmBx7?8ur&nCiV8E57h#iu-R^=#7nmf4(xC{7VJdp9xrycb3)3tKOIAU+R`7? zV;#6VI4#sTtl7t8OZ#S(-)>nSs?}Rm+lI|8a+b~6?0}guvt};)FniC;sO(@iX3m=F z?0qwD8uq+%GyNaKl4FjVY4cq+mW`VeW+t0(%tvOHhZ!E{@x;x%eLD%?1^D}J&`YHn##rt#%Q&o|kSKsF(6Wmvl|(C^F;gtZ7K<;+td$X(lhb?gSYepaTTEtf#)K)5S z6dwo8Nz-u5ae^Hn_*@=TYFF00Ki?rTu+i4a{H8>kqCe8V(IBW07#$p#y%6yGTliewW zn1OOWW=g85vJL!7fJu&^t*$LdQSYJ*HbYJ^DR7eM?}8AkO%B{)tA|sN>k=V{MPR;B n{#Q@Y1h$cw(af__E31Y-f{Sk0LiUh}PifSoSmkRk`*-jU^5=(s literal 0 HcmV?d00001 diff --git a/dashboard/horizon/locale/bg_BG/LC_MESSAGES/django.po b/dashboard/horizon/locale/bg_BG/LC_MESSAGES/django.po new file mode 100644 index 00000000..eada8d2d --- /dev/null +++ b/dashboard/horizon/locale/bg_BG/LC_MESSAGES/django.po @@ -0,0 +1,521 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Dimitar Dimitrov , 2012. +msgid "" +msgstr "" +"Project-Id-Version: Horizon\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:08+0000\n" +"PO-Revision-Date: 2012-08-16 08:19+0000\n" +"Last-Translator: Dimitar Dimitrov \n" +"Language-Team: LANGUAGE \n" +"Language: bg_BG\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: base.py:424 +msgid "Other" +msgstr "Друго" + +#: decorators.py:55 +msgid "Please log in to continue." +msgstr "Моля, влезте за да продължите." + +#: decorators.py:87 +#, python-format +msgid "You are not authorized to access %s" +msgstr "Вие не сте оторизирани да достъпвате %s" + +#: exceptions.py:283 +msgid "Unauthorized. Please try logging in again." +msgstr "Не сте оторизирани. Моля, опитайте да влезете отново." + +#: browsers/base.py:90 +msgid "Navigation Item" +msgstr "" + +#: browsers/views.py:42 +#, python-format +msgid "Select a %s to browse." +msgstr "" + +#: conf/default.py:29 +msgid "Password is not accepted" +msgstr "Паролата не е одобрена." + +#: tables/actions.py:349 +msgid "Filter" +msgstr "Филтър" + +#: tables/actions.py:527 +#, python-format +msgid "%(action)s %(data_type)s" +msgstr "" + +#: tables/actions.py:561 +msgid "N/A" +msgstr "" + +#: tables/actions.py:589 +#, python-format +msgid "You do not have permission to %(action)s: %(objs)s" +msgstr "Вие нямате права да %(action)s: %(objs)s" + +#: tables/actions.py:595 +#, python-format +msgid "Unable to %(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:601 +#, python-format +msgid "%(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:611 +msgid "Delete" +msgstr "Изтриване" + +#: tables/actions.py:612 +msgid "Deleted" +msgstr "Изтрит" + +#: tables/base.py:275 +#, python-format +msgid "The attribute %(attr)s doesn't exist on %(obj)s." +msgstr "" + +#: tables/base.py:748 +msgid "No items to display." +msgstr "" + +#: tables/base.py:852 +msgid "Actions" +msgstr "Действия" + +#: tables/base.py:1035 +#, python-format +msgid "No match returned for the id \"%s\"." +msgstr "" + +#: tables/base.py:1165 +msgid "Please select a row before taking that action." +msgstr "Моля, изберете ред преди да предприемете това действие." + +#: templates/_header.html:3 +msgid "Logged in as" +msgstr "" + +#: templates/_header.html:5 +msgid "Help" +msgstr "" + +#: templates/_header.html:7 +msgid "Sign Out" +msgstr "" + +#: templates/splash.html:7 templates/auth/login.html:4 +msgid "Login" +msgstr "" + +#: templates/auth/_login.html:4 +msgid "Log In" +msgstr "" + +#: templates/auth/_login.html:14 +#, fuzzy +msgid "You don't have permissions to access:" +msgstr "Вие нямате права да %(action)s: %(objs)s" + +#: templates/auth/_login.html:16 +msgid "Login as different user or go back to" +msgstr "" + +#: templates/auth/_login.html:17 +msgid "home page" +msgstr "" + +#: templates/auth/_login.html:27 +msgid "Sign In" +msgstr "" + +#: templates/horizon/_messages.html:7 +msgid "Info: " +msgstr "Информация: " + +#: templates/horizon/_messages.html:13 +msgid "Warning: " +msgstr "Внимание: " + +#: templates/horizon/_messages.html:19 +msgid "Success: " +msgstr "Успех: " + +#: templates/horizon/_messages.html:25 +msgid "Error: " +msgstr "Грешка: " + +#: templates/horizon/common/_data_table.html:54 +msgid "Summary" +msgstr "" + +#: templates/horizon/common/_data_table.html:63 +#, python-format +msgid "Displaying %(counter)s item" +msgid_plural "Displaying %(counter)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_data_table_row_actions.html:10 +msgid "More" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:4 +#, fuzzy +msgid "Quota Summary" +msgstr "Обобщение на потреблението" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "Used" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "of" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#, fuzzy +msgid "Available Instances" +msgstr "Всички инстанции" + +#: templates/horizon/common/_quota_summary.html:8 +#, fuzzy +msgid "Available vCPUs" +msgstr "Наличен" + +#: templates/horizon/common/_quota_summary.html:11 +#, fuzzy +msgid "Available RAM" +msgstr "Наличен" + +#: templates/horizon/common/_quota_summary.html:15 +#, fuzzy +msgid "Available volumes" +msgstr "Наличен" + +#: templates/horizon/common/_quota_summary.html:18 +#, fuzzy +msgid "Available volume storage" +msgstr "Всички инстанции" + +#: templates/horizon/common/_resource_browser.html:10 +#, python-format +msgid "Displaying %(nav_items)s item" +msgid_plural "Displaying %(nav_items)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_resource_browser.html:11 +#, python-format +msgid "Displaying %(content_items)s item" +msgid_plural "Displaying %(content_items)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_sidebar.html:14 +msgid "Current Project" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:5 +msgid "Select a month to query its usage" +msgstr "Изберете месец за да намерите потреблението" + +#: templates/horizon/common/_usage_summary.html:9 +msgid "Submit" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:14 +msgid "Active Instances" +msgstr "Активни инстанции" + +#: templates/horizon/common/_usage_summary.html:15 +#, fuzzy +msgid "Active RAM" +msgstr "Активна памет" + +#: templates/horizon/common/_usage_summary.html:16 +msgid "This Month's VCPU-Hours" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:17 +msgid "This Month's GB-Hours" +msgstr "" + +#: templates/horizon/common/_workflow.html:33 +msgid "Cancel" +msgstr "Откажи" + +#: templatetags/branding.py:35 +msgid "Horizon" +msgstr "" + +#: templatetags/horizon.py:109 +msgid "No Limit" +msgstr "Без лимит" + +#: templatetags/horizon.py:111 templatetags/horizon.py:113 +msgid "Available" +msgstr "Наличен" + +#: templatetags/sizeformat.py:45 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "" +msgstr[1] "" + +#: templatetags/sizeformat.py:49 +#, python-format +msgid "%(size)d" +msgid_plural "%(size)d" +msgstr[0] "" +msgstr[1] "" + +#: templatetags/sizeformat.py:52 +#, python-format +msgid "%s KB" +msgstr "" + +#: templatetags/sizeformat.py:55 +#, python-format +msgid "%s MB" +msgstr "" + +#: templatetags/sizeformat.py:58 +#, python-format +msgid "%s GB" +msgstr "" + +#: templatetags/sizeformat.py:61 +#, python-format +msgid "%s TB" +msgstr "" + +#: templatetags/sizeformat.py:63 +#, python-format +msgid "%s PB" +msgstr "" + +#: test/settings.py:114 +msgid "Password must be between 8 and 18 characters." +msgstr "Паролата трябва да е между 8 и 18 символа." + +#: test/test_dashboards/cats/dashboard.py:8 +msgid "Cute Cats" +msgstr "" + +#: test/test_dashboards/cats/dashboard.py:14 +msgid "Fierce Cats" +msgstr "" + +#: test/test_dashboards/cats/dashboard.py:19 +msgid "Cats" +msgstr "" + +#: test/test_dashboards/cats/kittens/panel.py:9 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:3 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:6 +msgid "Kittens" +msgstr "" + +#: test/test_dashboards/cats/tigers/panel.py:9 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:3 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:6 +msgid "Tigers" +msgstr "" + +#: test/test_dashboards/dogs/dashboard.py:7 +msgid "Dogs" +msgstr "" + +#: test/test_dashboards/dogs/puppies/panel.py:9 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:3 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:6 +msgid "Puppies" +msgstr "" + +#: test/tests/base.py:39 +msgid "My Dashboard" +msgstr "" + +#: test/tests/base.py:45 +msgid "My Panel" +msgstr "Моят панел" + +#: test/tests/base.py:51 +msgid "Admin Panel" +msgstr "Админ панел" + +#: test/tests/messages.py:32 +msgid "Giant ants are attacking San Francisco!" +msgstr "" + +#: test/tests/messages.py:46 +msgid "We are now safe from ants! Go here!" +msgstr "" + +#: test/tests/tables.py:107 +msgid "Batch" +msgstr "" + +#: test/tests/tables.py:108 +msgid "Batched" +msgstr "" + +#: test/tests/tables.py:109 test/tests/tables.py:120 +msgid "Item" +msgstr "" + +#: test/tests/tables.py:110 test/tests/tables.py:121 +msgid "Items" +msgstr "" + +#: test/tests/tables.py:118 +msgid "Down" +msgstr "" + +#: test/tests/tables.py:118 +msgid "Up" +msgstr "" + +#: test/tests/tables.py:119 +msgid "Downed" +msgstr "" + +#: test/tests/tables.py:119 +msgid "Upped" +msgstr "" + +#: test/tests/tables.py:187 +msgid "No Actions Table" +msgstr "" + +#: test/tests/tables.py:684 +msgid "Single Table" +msgstr "" + +#: test/tests/tabs.py:36 +msgid "Tab One" +msgstr "" + +#: test/tests/tabs.py:42 +msgid "Delayed Tab" +msgstr "" + +#: test/tests/tabs.py:49 +msgid "Disabled Tab" +msgstr "" + +#: test/tests/tabs.py:58 +msgid "Disallowed Tab" +msgstr "" + +#: test/tests/tabs.py:76 +msgid "Tab With My Table" +msgstr "" + +#: test/tests/tabs.py:85 +msgid "Recoverable Error Tab" +msgstr "" + +#: test/tests/workflows.py:43 +msgid "Project" +msgstr "Проект" + +#: test/tests/workflows.py:44 +msgid "User" +msgstr "Потребител" + +#: test/tests/workflows.py:47 +msgid "Test Action One" +msgstr "" + +#: test/tests/workflows.py:61 +msgid "Instance" +msgstr "Инстанция" + +#: test/tests/workflows.py:64 +msgid "Test Action Two" +msgstr "" + +#: test/tests/workflows.py:72 +msgid "Test Action Three" +msgstr "" + +#: test/tests/workflows.py:77 +msgid "Admin" +msgstr "Админ" + +#: test/tests/workflows.py:80 +msgid "Admin Action" +msgstr "" + +#: utils/fields.py:46 +msgid "Incorrect format for IP address" +msgstr "Невалиден формат за IP адрес" + +#: utils/fields.py:47 +msgid "Invalid version for IP address" +msgstr "Невалидна версия за IP адрес" + +#: utils/fields.py:48 +msgid "Invalid subnet mask" +msgstr "Невалидна маска на подмрежата" + +#: workflows/base.py:71 +msgid "Processing..." +msgstr "Обработване..." + +#: workflows/base.py:467 +#, fuzzy +msgid "All available" +msgstr "Наличен" + +#: workflows/base.py:468 +msgid "Members" +msgstr "" + +#: workflows/base.py:469 +#, fuzzy +msgid "None available." +msgstr "Наличен" + +#: workflows/base.py:470 +msgid "No members." +msgstr "" + +#: workflows/base.py:569 +msgid "Save" +msgstr "Запазване" + +#: workflows/base.py:570 +#, python-format +msgid "%s completed successfully." +msgstr "%s приключи успешно." + +#: workflows/base.py:571 +#, python-format +msgid "%s did not complete." +msgstr "%s не приключи." diff --git a/dashboard/horizon/locale/bg_BG/LC_MESSAGES/djangojs.mo b/dashboard/horizon/locale/bg_BG/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000000000000000000000000000000000000..834b8fbe8d7dab57d6f58b1fa22a8e2e70d2e521 GIT binary patch literal 378 zcmYL^K~KUk7=|%=+R?Lz9=zd)8;J>2G*nh_vE9fRBzmh*XAQQbD@K2af6w3Ix5UVs zJn5m&*S_D+>B)BobA+5B=g2W~fwUPQW4s*V**gE3d-tZ-0kRFOv~P^Fl+4-Wfz3Es z@p6$Bc~t71^8sX2n+@FU>jo-ENi;7in0T!M4neAdrlcPyeMn-6uShba{b4u8pa`Y@ zFzw@i-Xmuv$O2ME?h7ij?o*NTNT?=2<&|x=DmYKcbSVVMndD@|CCkK^rD@3*i(6D} zy;rq{N~?ED1j}d3w(>}b@z(U5uMZAZm6_y+{gPfBO`Hwd;&)#=>>c!HxZG`(vDak% fs(hov&9a)p;NZ2jUbF{p*KT8MP08TdgsAfiRQqN_ literal 0 HcmV?d00001 diff --git a/dashboard/horizon/locale/bg_BG/LC_MESSAGES/djangojs.po b/dashboard/horizon/locale/bg_BG/LC_MESSAGES/djangojs.po new file mode 100644 index 00000000..65b0ef3e --- /dev/null +++ b/dashboard/horizon/locale/bg_BG/LC_MESSAGES/djangojs.po @@ -0,0 +1,73 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:09+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: static/horizon/js/horizon.forms.js:47 +msgid "Additional information here..." +msgstr "" + +#: static/horizon/js/horizon.forms.js:53 +msgid "Filter" +msgstr "" + +#: static/horizon/js/horizon.instances.js:28 +msgid "There was a problem communicating with the server, please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:125 +msgid "There was an error submitting the form. Please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:159 static/horizon/js/horizon.tabs.js:9 +msgid "Loading" +msgstr "" + +#: static/horizon/js/horizon.modals.js:178 +msgid "An error occurred. Please try again." +msgstr "" + +#: static/horizon/js/horizon.tables.js:47 +msgid "An error occurred while updating." +msgstr "" + +#: static/horizon/js/horizon.tables.js:145 +msgid "You have selected " +msgstr "" + +#: static/horizon/js/horizon.tables.js:158 +msgid "Confirm " +msgstr "" + +#: static/horizon/js/horizon.tables.js:159 +msgid "Please confirm your selection. This action cannot be undone." +msgstr "" + +#: static/horizon/js/horizon.tables.js:173 +msgid "Working" +msgstr "" + +#: static/horizon/js/horizon.tables.js:216 +#, c-format +msgid "Displaying %s item" +msgid_plural "Displaying %s items" +msgstr[0] "" +msgstr[1] "" + +#: static/horizon/js/horizon.users.js:18 +msgid "Passwords do not match." +msgstr "" diff --git a/dashboard/horizon/locale/en/LC_MESSAGES/django.mo b/dashboard/horizon/locale/en/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..233df8ed78c0186721cf3f43d50812ab8382ecf8 GIT binary patch literal 4746 zcmeH|&2J>d6~HTD1H`Z)d}S9%aP0lR<@Ev$3d>_iV51|Y> zc*}rym%Oj!aLEyPKjV%;(X#}3)CH=q!WI-g9^MY0hqu8O;g{jh%J08`JnC0eEUhlV z1Mqct2mBqB@qd9b{|c0G??E2*0hQ?g5Bv)J1Wvz6BqI|A1oWRVe%X6pG%PFjnlj4br6!mplT+PmjQ(@JT51 zn(#2}zyQ7g#jmfy3cL&t!cU;+zh!8^JK(*v4?(dbfJfjIlzlcJQ`7}0^K8j3lz8V* z{QF|r{(0H{4V<9=O(^>R4G%$amgqeUW&gwQ5qJ`kS}KCFpBRcg8I<+=@SE@jI0b(V zU;Tnoe~03aAFw&G=ciEe?^P&z{#5d$WQ0Bc1MgK1$kGe!9`S*J$ zdOj|B70SF%q0Bpga?y7?Ec^;bX%9omzvZ&O4JD5fDEZfi;{P8*(f?a0`+Nf(6hHkFO8$KWW!^nFOY-l27{KFD{AyqY{t)6?bqR|8H%tB*N`AZp#g2bL z$-fVv?B`~JDtrnJwhO+-FQ1b6X zDEarmU4#8R2*sX@|e(G^V_CJP*efJ?!>I0~!pp5?pBJ0UovOmIC zlxIqwD)|^ZhCGctRQAomHDm?Ri0o$;k^PBHV)toe2pLDDNX*3F-$s@YDUv5r2ZFX~27x6*Dew%&%?<*k-= zZZnVLor+8eqfjTQ*SkY1YOb)y72LLZC2`&)Oj2`UH%bPz9tosPWi%Dm6}=F~#kma3l=6=p7GS(??fT8eDeDver-V)Uuy z$Y3+#w8^YC-ka8zEV*tHy_B)P$hFcVY9(oP+OlO`KZY(F?|PnyJY`X$+$AOsdaD z-N-9m#2ZfglwcPgsYoaGwK_VpKF<=IDeL%-)tGa1#2u}uHCmaf8|V6I7V5|qX<|s9 z9*MO7p_@Ceo0iA-txfbajtceUv~G1wMsBl@imJ!fIIH8dt)oONX{CvelHBfna>N|R z8J(qlbYYIw-V|Q;9b7p$B@E9}NnpHRsT8dukLr1^7m=dpY%9fck}kRkXpv9rX4|Uu z-37WV-4PqUpWAE)=Qy1^(`H286o;)3jtGHT&%0fd?I?0muO+tN`H0DQnLaYTVcGoP zEDT0m({6M!d;N$;KUEEUOaf(5Q&OMP&pQblrq(5+UfYWi8i{*x7mVY&MhDlgip`^P z%!AWup1JFLpPjF7?Dj`(9IiH!!Ub&dGw&BZ=f0C>Try!rm%;Hkg8;Q9u?doCn<%NM zjUG>rfNwa`;k?NbzT=zkq z48ckmJUci6HGNtPO_QN>wwGo;Sat0v3{K^37c@9dL-n;rFrQf?w`j0nyshb}iOH#8 zaw3>`Oi#|#re=;!P#74Tv)hrZf9<%5>0t74JuzLIEXSQS&Ib*SwTq3HJ7L*0v&hE! zbe_d_M?cXXw4ZW>am$*%t#Esu96D=~c1|J%4QskJy_mG)$aVBs$NOHbTJ87y6)#$& zO?y#hRC|M4&{ccYw8v4FYo!{OTBKLX`2kKuB1L-+ l=)vpkSfY>2>dEmFL+Ud#>c$`I#vkj(AM3^+>;LbM^, 2012. +# , 2012. +msgid "" +msgstr "" +"Project-Id-Version: Horizon\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:08+0000\n" +"PO-Revision-Date: 2012-08-16 08:19+0000\n" +"Last-Translator: Gabriel Hurley \n" +"Language-Team: English (http://www.transifex.com/projects/p/openstack/" +"language/en/)\n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: base.py:424 +msgid "Other" +msgstr "Other" + +#: decorators.py:55 +msgid "Please log in to continue." +msgstr "Please log in to continue." + +#: decorators.py:87 +#, python-format +msgid "You are not authorized to access %s" +msgstr "You are not authorized to access %s" + +#: exceptions.py:283 +msgid "Unauthorized. Please try logging in again." +msgstr "Unauthorized. Please try logging in again." + +#: browsers/base.py:90 +msgid "Navigation Item" +msgstr "" + +#: browsers/views.py:42 +#, python-format +msgid "Select a %s to browse." +msgstr "" + +#: conf/default.py:29 +msgid "Password is not accepted" +msgstr "Password is not accepted" + +#: tables/actions.py:349 +msgid "Filter" +msgstr "Filter" + +#: tables/actions.py:527 +#, python-format +msgid "%(action)s %(data_type)s" +msgstr "" + +#: tables/actions.py:561 +msgid "N/A" +msgstr "" + +#: tables/actions.py:589 +#, python-format +msgid "You do not have permission to %(action)s: %(objs)s" +msgstr "You do not have permission to %(action)s: %(objs)s" + +#: tables/actions.py:595 +#, python-format +msgid "Unable to %(action)s: %(objs)s" +msgstr "Unable to %(action)s: %(objs)s" + +#: tables/actions.py:601 +#, python-format +msgid "%(action)s: %(objs)s" +msgstr "%(action)s: %(objs)s" + +#: tables/actions.py:611 +msgid "Delete" +msgstr "Delete" + +#: tables/actions.py:612 +msgid "Deleted" +msgstr "Deleted" + +#: tables/base.py:275 +#, python-format +msgid "The attribute %(attr)s doesn't exist on %(obj)s." +msgstr "The attribute %(attr)s doesn't exist on %(obj)s." + +#: tables/base.py:748 +msgid "No items to display." +msgstr "No items to display." + +#: tables/base.py:852 +msgid "Actions" +msgstr "Actions" + +#: tables/base.py:1035 +#, python-format +msgid "No match returned for the id \"%s\"." +msgstr "No match returned for the id \"%s\"." + +#: tables/base.py:1165 +msgid "Please select a row before taking that action." +msgstr "Please select a row before taking that action." + +#: templates/_header.html:3 +msgid "Logged in as" +msgstr "" + +#: templates/_header.html:5 +msgid "Help" +msgstr "" + +#: templates/_header.html:7 +msgid "Sign Out" +msgstr "" + +#: templates/splash.html:7 templates/auth/login.html:4 +msgid "Login" +msgstr "" + +#: templates/auth/_login.html:4 +msgid "Log In" +msgstr "" + +#: templates/auth/_login.html:14 +#, fuzzy +msgid "You don't have permissions to access:" +msgstr "You do not have permission to %(action)s: %(objs)s" + +#: templates/auth/_login.html:16 +msgid "Login as different user or go back to" +msgstr "" + +#: templates/auth/_login.html:17 +msgid "home page" +msgstr "" + +#: templates/auth/_login.html:27 +msgid "Sign In" +msgstr "" + +#: templates/horizon/_messages.html:7 +msgid "Info: " +msgstr "Info: " + +#: templates/horizon/_messages.html:13 +msgid "Warning: " +msgstr "Warning: " + +#: templates/horizon/_messages.html:19 +msgid "Success: " +msgstr "Success: " + +#: templates/horizon/_messages.html:25 +msgid "Error: " +msgstr "Error: " + +#: templates/horizon/common/_data_table.html:54 +msgid "Summary" +msgstr "Summary" + +#: templates/horizon/common/_data_table.html:63 +#, python-format +msgid "Displaying %(counter)s item" +msgid_plural "Displaying %(counter)s items" +msgstr[0] "Displaying %(counter)s item" +msgstr[1] "Displaying %(counter)s items" + +#: templates/horizon/common/_data_table_row_actions.html:10 +msgid "More" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:4 +#, fuzzy +msgid "Quota Summary" +msgstr "Summary" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "Used" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "of" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#, fuzzy +msgid "Available Instances" +msgstr "All Instances" + +#: templates/horizon/common/_quota_summary.html:8 +#, fuzzy +msgid "Available vCPUs" +msgstr "Available" + +#: templates/horizon/common/_quota_summary.html:11 +#, fuzzy +msgid "Available RAM" +msgstr "Available" + +#: templates/horizon/common/_quota_summary.html:15 +#, fuzzy +msgid "Available volumes" +msgstr "Available" + +#: templates/horizon/common/_quota_summary.html:18 +#, fuzzy +msgid "Available volume storage" +msgstr "All Instances" + +#: templates/horizon/common/_resource_browser.html:10 +#, python-format +msgid "Displaying %(nav_items)s item" +msgid_plural "Displaying %(nav_items)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_resource_browser.html:11 +#, python-format +msgid "Displaying %(content_items)s item" +msgid_plural "Displaying %(content_items)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_sidebar.html:14 +msgid "Current Project" +msgstr "Current Project" + +#: templates/horizon/common/_usage_summary.html:5 +msgid "Select a month to query its usage" +msgstr "Select a month to query its usage" + +#: templates/horizon/common/_usage_summary.html:9 +msgid "Submit" +msgstr "Submit" + +#: templates/horizon/common/_usage_summary.html:14 +msgid "Active Instances" +msgstr "Active Instances" + +#: templates/horizon/common/_usage_summary.html:15 +#, fuzzy +msgid "Active RAM" +msgstr "Active Memory" + +#: templates/horizon/common/_usage_summary.html:16 +msgid "This Month's VCPU-Hours" +msgstr "This Month's VCPU-Hours" + +#: templates/horizon/common/_usage_summary.html:17 +msgid "This Month's GB-Hours" +msgstr "This Month's GB-Hours" + +#: templates/horizon/common/_workflow.html:33 +msgid "Cancel" +msgstr "Cancel" + +#: templatetags/branding.py:35 +msgid "Horizon" +msgstr "" + +#: templatetags/horizon.py:109 +msgid "No Limit" +msgstr "No Limit" + +#: templatetags/horizon.py:111 templatetags/horizon.py:113 +msgid "Available" +msgstr "Available" + +#: templatetags/sizeformat.py:45 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "%(size)d byte" +msgstr[1] "%(size)d bytes" + +#: templatetags/sizeformat.py:49 +#, python-format +msgid "%(size)d" +msgid_plural "%(size)d" +msgstr[0] "%(size)d" +msgstr[1] "%(size)d" + +#: templatetags/sizeformat.py:52 +#, python-format +msgid "%s KB" +msgstr "%s KB" + +#: templatetags/sizeformat.py:55 +#, python-format +msgid "%s MB" +msgstr "%s MB" + +#: templatetags/sizeformat.py:58 +#, python-format +msgid "%s GB" +msgstr "%s GB" + +#: templatetags/sizeformat.py:61 +#, python-format +msgid "%s TB" +msgstr "%s TB" + +#: templatetags/sizeformat.py:63 +#, python-format +msgid "%s PB" +msgstr "%s PB" + +#: test/settings.py:114 +msgid "Password must be between 8 and 18 characters." +msgstr "Password must be between 8 and 18 characters." + +#: test/test_dashboards/cats/dashboard.py:8 +msgid "Cute Cats" +msgstr "Cute Cats" + +#: test/test_dashboards/cats/dashboard.py:14 +msgid "Fierce Cats" +msgstr "Fierce Cats" + +#: test/test_dashboards/cats/dashboard.py:19 +msgid "Cats" +msgstr "Cats" + +#: test/test_dashboards/cats/kittens/panel.py:9 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:3 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:6 +msgid "Kittens" +msgstr "Kittens" + +#: test/test_dashboards/cats/tigers/panel.py:9 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:3 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:6 +msgid "Tigers" +msgstr "Tigers" + +#: test/test_dashboards/dogs/dashboard.py:7 +msgid "Dogs" +msgstr "Dogs" + +#: test/test_dashboards/dogs/puppies/panel.py:9 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:3 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:6 +msgid "Puppies" +msgstr "Puppies" + +#: test/tests/base.py:39 +msgid "My Dashboard" +msgstr "My Dashboard" + +#: test/tests/base.py:45 +msgid "My Panel" +msgstr "My Panel" + +#: test/tests/base.py:51 +msgid "Admin Panel" +msgstr "Admin Panel" + +#: test/tests/messages.py:32 +msgid "Giant ants are attacking San Francisco!" +msgstr "Giant ants are attacking San Francisco!" + +#: test/tests/messages.py:46 +msgid "We are now safe from ants! Go here!" +msgstr "" + +#: test/tests/tables.py:107 +msgid "Batch" +msgstr "Batch" + +#: test/tests/tables.py:108 +msgid "Batched" +msgstr "Batched" + +#: test/tests/tables.py:109 test/tests/tables.py:120 +msgid "Item" +msgstr "Item" + +#: test/tests/tables.py:110 test/tests/tables.py:121 +msgid "Items" +msgstr "Items" + +#: test/tests/tables.py:118 +msgid "Down" +msgstr "Down" + +#: test/tests/tables.py:118 +msgid "Up" +msgstr "Up" + +#: test/tests/tables.py:119 +msgid "Downed" +msgstr "Downed" + +#: test/tests/tables.py:119 +msgid "Upped" +msgstr "Upped" + +#: test/tests/tables.py:187 +msgid "No Actions Table" +msgstr "" + +#: test/tests/tables.py:684 +msgid "Single Table" +msgstr "" + +#: test/tests/tabs.py:36 +msgid "Tab One" +msgstr "Tab One" + +#: test/tests/tabs.py:42 +msgid "Delayed Tab" +msgstr "Delayed Tab" + +#: test/tests/tabs.py:49 +msgid "Disabled Tab" +msgstr "Disabled Tab" + +#: test/tests/tabs.py:58 +msgid "Disallowed Tab" +msgstr "Disallowed Tab" + +#: test/tests/tabs.py:76 +msgid "Tab With My Table" +msgstr "Tab With My Table" + +#: test/tests/tabs.py:85 +msgid "Recoverable Error Tab" +msgstr "Recoverable Error Tab" + +#: test/tests/workflows.py:43 +msgid "Project" +msgstr "Project" + +#: test/tests/workflows.py:44 +msgid "User" +msgstr "User" + +#: test/tests/workflows.py:47 +msgid "Test Action One" +msgstr "Test Action One" + +#: test/tests/workflows.py:61 +msgid "Instance" +msgstr "Instance" + +#: test/tests/workflows.py:64 +msgid "Test Action Two" +msgstr "Test Action Two" + +#: test/tests/workflows.py:72 +msgid "Test Action Three" +msgstr "Test Action Three" + +#: test/tests/workflows.py:77 +msgid "Admin" +msgstr "Admin" + +#: test/tests/workflows.py:80 +msgid "Admin Action" +msgstr "Admin Action" + +#: utils/fields.py:46 +msgid "Incorrect format for IP address" +msgstr "Incorrect format for IP address" + +#: utils/fields.py:47 +msgid "Invalid version for IP address" +msgstr "Invalid version for IP address" + +#: utils/fields.py:48 +msgid "Invalid subnet mask" +msgstr "Invalid subnet mask" + +#: workflows/base.py:71 +msgid "Processing..." +msgstr "Processing..." + +#: workflows/base.py:467 +#, fuzzy +msgid "All available" +msgstr "Available" + +#: workflows/base.py:468 +msgid "Members" +msgstr "" + +#: workflows/base.py:469 +#, fuzzy +msgid "None available." +msgstr "Available" + +#: workflows/base.py:470 +msgid "No members." +msgstr "" + +#: workflows/base.py:569 +msgid "Save" +msgstr "Save" + +#: workflows/base.py:570 +#, python-format +msgid "%s completed successfully." +msgstr "%s completed successfully." + +#: workflows/base.py:571 +#, python-format +msgid "%s did not complete." +msgstr "%s did not complete." diff --git a/dashboard/horizon/locale/en/LC_MESSAGES/djangojs.mo b/dashboard/horizon/locale/en/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000000000000000000000000000000000000..834b8fbe8d7dab57d6f58b1fa22a8e2e70d2e521 GIT binary patch literal 378 zcmYL^K~KUk7=|%=+R?Lz9=zd)8;J>2G*nh_vE9fRBzmh*XAQQbD@K2af6w3Ix5UVs zJn5m&*S_D+>B)BobA+5B=g2W~fwUPQW4s*V**gE3d-tZ-0kRFOv~P^Fl+4-Wfz3Es z@p6$Bc~t71^8sX2n+@FU>jo-ENi;7in0T!M4neAdrlcPyeMn-6uShba{b4u8pa`Y@ zFzw@i-Xmuv$O2ME?h7ij?o*NTNT?=2<&|x=DmYKcbSVVMndD@|CCkK^rD@3*i(6D} zy;rq{N~?ED1j}d3w(>}b@z(U5uMZAZm6_y+{gPfBO`Hwd;&)#=>>c!HxZG`(vDak% fs(hov&9a)p;NZ2jUbF{p*KT8MP08TdgsAfiRQqN_ literal 0 HcmV?d00001 diff --git a/dashboard/horizon/locale/en/LC_MESSAGES/djangojs.po b/dashboard/horizon/locale/en/LC_MESSAGES/djangojs.po new file mode 100644 index 00000000..65b0ef3e --- /dev/null +++ b/dashboard/horizon/locale/en/LC_MESSAGES/djangojs.po @@ -0,0 +1,73 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:09+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: static/horizon/js/horizon.forms.js:47 +msgid "Additional information here..." +msgstr "" + +#: static/horizon/js/horizon.forms.js:53 +msgid "Filter" +msgstr "" + +#: static/horizon/js/horizon.instances.js:28 +msgid "There was a problem communicating with the server, please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:125 +msgid "There was an error submitting the form. Please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:159 static/horizon/js/horizon.tabs.js:9 +msgid "Loading" +msgstr "" + +#: static/horizon/js/horizon.modals.js:178 +msgid "An error occurred. Please try again." +msgstr "" + +#: static/horizon/js/horizon.tables.js:47 +msgid "An error occurred while updating." +msgstr "" + +#: static/horizon/js/horizon.tables.js:145 +msgid "You have selected " +msgstr "" + +#: static/horizon/js/horizon.tables.js:158 +msgid "Confirm " +msgstr "" + +#: static/horizon/js/horizon.tables.js:159 +msgid "Please confirm your selection. This action cannot be undone." +msgstr "" + +#: static/horizon/js/horizon.tables.js:173 +msgid "Working" +msgstr "" + +#: static/horizon/js/horizon.tables.js:216 +#, c-format +msgid "Displaying %s item" +msgid_plural "Displaying %s items" +msgstr[0] "" +msgstr[1] "" + +#: static/horizon/js/horizon.users.js:18 +msgid "Passwords do not match." +msgstr "" diff --git a/dashboard/horizon/locale/es/LC_MESSAGES/django.mo b/dashboard/horizon/locale/es/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..26b6e77ecc39aff8b4208f351220fcd1a67cafa7 GIT binary patch literal 5039 zcmc(iU2Gjk6@UktKyh2zCZv>~!Z;8Tx4AdA(>Adar}1ALH@4%(b}AsEntSi~deXhS zw>!HxbtEJnK>P?Pc>swAUV_>O1XWdvpbF)I7>OT&Kp-lFl!^)os!AYKi9YbecV^eW zO4>(8p8f95ocTNF%sJ~0 zDC5sTIbRHSzzb0J^))E#U52PaeHV({56k|aLb1yV6utft%6VUdqLdsE7#>7; z5T1u3_bilkz7K;vN?wIB-?j4n_i#V&e}*Ezoy#Kfw?Wa0Dx!rP(jYZuh;J}BdkmHka9`)@PpC~~_vjI9YNCDChbd6!|Yg+)$mQev&F@Jw_d; zj#8x@DWFc2l=a1SaxX-O;!msPBm6i>9itwieu^q(h${C*imWfX7JD40N*ShBskp;h zIlcC)_#u8Lx{`BEQf2?$R5?H9Qaw~Ds3PcE>>|1pyYy2Zp*}#BB0eT@WSaUAwL)Dj zs3a8eZ@JUr4^qT#eN?gQZmQU$izMV7FDsfnf+~?IYz>iZ@+^Z;i_L+L-lDNm~J^LCaGf!m8ZQJA3ddDC=N$tE3jow|f zJq>!d{Lr=K%&s+uH|FqqVnm*&!gD%2XTx(oJnKoT9oft_w9o5x>-}OLMaxwg(r^tO zCz)OqsjA_yQ6Jc`dNTHzi5aAZ8!Z!)ragnJ-R*i~C1TwEi^E9<_rf1XS zysc-vXI6)vF)O=J1sZD1`F3QMUEJj8^(4+1^@Pi83ms_TJ4^VjB0009HyOUMZ_B{g zblzs=t-TCRn)0|1hlg#b@ia}+L9I?Wo7T(Yo^TPHQ4`Lf4XW2Bwc2EvsXr-)o-?sN zk)k>0>q)mdX`{9}nWXMw606C$o}k8hrWcd6Wdii%tTv5CiUH|gOjvH!I{fIE2wVER z7TZj>jDK>itz%P<@5xM?rVtHJin`S)m!W*Grjn+fj0H`c#l_l$Hq1V~tjCOBswF0E z$V=e>GY5v%jOn9U$fQ+QD7YI3WH9{k4L8;SMt-b~x49ZJCzA(r%@fcYg9I4ukBl`v5` zndGV8+QcoSCqm6l@oe#&rg3pqEwuS- zW3~m4Q6D#HOh4!ObdrbLAhBnCGFed!?Fo}U$TX5*{Uy|^+cs@EEWq(@wzD$kR3;mh zvxV0W>hel;ow4mC%_`Hr=^B-hyy+|RT%oSn)ANi*uo{z_kOrS~D- zH!#>g@IWsG!O9uiakBnKx!!?F?}I_^P$4&Ed{&v~Ui-*o5)_7`noZg9bP~DP=+UHR zqR4uE)I_yvD>NQ&wg{zFqVTb0n34bMz03{Q;fqf=AIBUeq*=DN(F zw!Uk6a(aCIxP8^$uF)cxR^~}EgF3TMX9tM1EbMPsmgokC~NFp=kCMsHW4PogWA($f?&`8vDB2&g}_9lt%odo=rDK@6Ix9QqEY-?KE zG;nJtC$!`^G*mO^6E$Maqf5V*_kt2ME+j#cEEsXVR9(BmJXshZ&m=7frR!totz*PYRAIhl<% zCdxEXQz8|kZEsgzFyiW@yCp*yC0(-HtpW{8$|Tv_RC6S9-C?~Pl4hARtt8DP)r+RX z)5WgtEK_=M|6&Ve1~ush!TpGkM#H_*vhzhg4CR{#g9l^qadOe5S{e)cttM3Qfij_n z`xD$OD1Rd7TLayP^eHnlUZ&V6!>4ldAE! z7dxjJn5@3;H(QL@B7QJz8`BazmihtwW6C-j;JR1|@{zapt z)lJRwPF$uiyY`OT-%h;55~eG@GJ1%zYySn{9%M$zvJ$oQMx)3)Tn4N8km*Q7OWCcF vuLfo%eS1{r628x!=U7Y>F5QOd3I$23ZQux^Q<2IXeo>}bHYfge!=3#Pg16bH literal 0 HcmV?d00001 diff --git a/dashboard/horizon/locale/es/LC_MESSAGES/django.po b/dashboard/horizon/locale/es/LC_MESSAGES/django.po new file mode 100644 index 00000000..f481e6b4 --- /dev/null +++ b/dashboard/horizon/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,524 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Alberto Molina Coballes , 2012. +# , 2012. +# Gabriel Hurley , 2012. +# Pedro Navarro Pérez , 2012. +msgid "" +msgstr "" +"Project-Id-Version: Horizon\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:08+0000\n" +"PO-Revision-Date: 2012-09-04 19:27+0000\n" +"Last-Translator: Alberto Molina Coballes \n" +"Language-Team: LANGUAGE \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: base.py:424 +msgid "Other" +msgstr "Otro" + +#: decorators.py:55 +msgid "Please log in to continue." +msgstr "Por favor inicie sesión para continuar." + +#: decorators.py:87 +#, python-format +msgid "You are not authorized to access %s" +msgstr "No está autorizado para acceder a %s" + +#: exceptions.py:283 +msgid "Unauthorized. Please try logging in again." +msgstr "No autorizado. Por favor ingrese de nuevo." + +#: browsers/base.py:90 +msgid "Navigation Item" +msgstr "Item de Navegación" + +#: browsers/views.py:42 +#, python-format +msgid "Select a %s to browse." +msgstr "Seleccionar una %s para navegar." + +#: conf/default.py:29 +msgid "Password is not accepted" +msgstr "La contraseña no se ha aceptado" + +#: tables/actions.py:349 +msgid "Filter" +msgstr "Filtrar" + +#: tables/actions.py:527 +#, python-format +msgid "%(action)s %(data_type)s" +msgstr "%(action)s %(data_type)s" + +#: tables/actions.py:561 +msgid "N/A" +msgstr "N/A" + +#: tables/actions.py:589 +#, python-format +msgid "You do not have permission to %(action)s: %(objs)s" +msgstr "No tiene permiso para %(action)s: %(objs)s" + +#: tables/actions.py:595 +#, python-format +msgid "Unable to %(action)s: %(objs)s" +msgstr "No ha sido posible %(action)s: %(objs)s" + +#: tables/actions.py:601 +#, python-format +msgid "%(action)s: %(objs)s" +msgstr "%(action)s: %(objs)s" + +#: tables/actions.py:611 +msgid "Delete" +msgstr "Borrar" + +#: tables/actions.py:612 +msgid "Deleted" +msgstr "Borrado" + +#: tables/base.py:275 +#, python-format +msgid "The attribute %(attr)s doesn't exist on %(obj)s." +msgstr "El atributo %(attr)s no existe en %(obj)s." + +#: tables/base.py:748 +msgid "No items to display." +msgstr "No hay ítems que mostrar" + +#: tables/base.py:852 +msgid "Actions" +msgstr "Acciones" + +#: tables/base.py:1035 +#, python-format +msgid "No match returned for the id \"%s\"." +msgstr "Ninguna coincidencia para el id \"%s\"." + +#: tables/base.py:1165 +msgid "Please select a row before taking that action." +msgstr "Por favor, seleccione una fila antes de realizar la acción." + +#: templates/_header.html:3 +msgid "Logged in as" +msgstr "Identificado como" + +#: templates/_header.html:5 +msgid "Help" +msgstr "Ayuda" + +#: templates/_header.html:7 +msgid "Sign Out" +msgstr "Salir" + +#: templates/splash.html:7 templates/auth/login.html:4 +msgid "Login" +msgstr "Ingresar" + +#: templates/auth/_login.html:4 +msgid "Log In" +msgstr "Ingresar" + +#: templates/auth/_login.html:14 +#, fuzzy +msgid "You don't have permissions to access:" +msgstr "No tiene permiso para %(action)s: %(objs)s" + +#: templates/auth/_login.html:16 +msgid "Login as different user or go back to" +msgstr "" + +#: templates/auth/_login.html:17 +msgid "home page" +msgstr "" + +#: templates/auth/_login.html:27 +msgid "Sign In" +msgstr "Ingresar" + +#: templates/horizon/_messages.html:7 +msgid "Info: " +msgstr "Info:" + +#: templates/horizon/_messages.html:13 +msgid "Warning: " +msgstr "Aviso:" + +#: templates/horizon/_messages.html:19 +msgid "Success: " +msgstr "Correcto:" + +#: templates/horizon/_messages.html:25 +msgid "Error: " +msgstr "Error: " + +#: templates/horizon/common/_data_table.html:54 +msgid "Summary" +msgstr "Resumen" + +#: templates/horizon/common/_data_table.html:63 +#, python-format +msgid "Displaying %(counter)s item" +msgid_plural "Displaying %(counter)s items" +msgstr[0] "Mostrando %(counter)s item" +msgstr[1] "Mostrando %(counter)s items" + +#: templates/horizon/common/_data_table_row_actions.html:10 +msgid "More" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:4 +#, fuzzy +msgid "Quota Summary" +msgstr "Resumen" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "Used" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "of" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#, fuzzy +msgid "Available Instances" +msgstr "Todas las Instancias" + +#: templates/horizon/common/_quota_summary.html:8 +#, fuzzy +msgid "Available vCPUs" +msgstr "Disponible" + +#: templates/horizon/common/_quota_summary.html:11 +#, fuzzy +msgid "Available RAM" +msgstr "Disponible" + +#: templates/horizon/common/_quota_summary.html:15 +#, fuzzy +msgid "Available volumes" +msgstr "Disponible" + +#: templates/horizon/common/_quota_summary.html:18 +#, fuzzy +msgid "Available volume storage" +msgstr "Todas las Instancias" + +#: templates/horizon/common/_resource_browser.html:10 +#, python-format +msgid "Displaying %(nav_items)s item" +msgid_plural "Displaying %(nav_items)s items" +msgstr[0] "Mostrando %(nav_items)s item" +msgstr[1] "Mostrando %(nav_items)s items" + +#: templates/horizon/common/_resource_browser.html:11 +#, python-format +msgid "Displaying %(content_items)s item" +msgid_plural "Displaying %(content_items)s items" +msgstr[0] "Mostrando %(content_items)s item" +msgstr[1] "Mostrando %(content_items)s items" + +#: templates/horizon/common/_sidebar.html:14 +msgid "Current Project" +msgstr "Proyecto Actual" + +#: templates/horizon/common/_usage_summary.html:5 +msgid "Select a month to query its usage" +msgstr "Seleccionar un mes para solicitar su uso" + +#: templates/horizon/common/_usage_summary.html:9 +msgid "Submit" +msgstr "Enviar" + +#: templates/horizon/common/_usage_summary.html:14 +msgid "Active Instances" +msgstr "Instancias Activas" + +#: templates/horizon/common/_usage_summary.html:15 +#, fuzzy +msgid "Active RAM" +msgstr "Memoria Activa" + +#: templates/horizon/common/_usage_summary.html:16 +msgid "This Month's VCPU-Hours" +msgstr "Horas VCPU de este mes" + +#: templates/horizon/common/_usage_summary.html:17 +msgid "This Month's GB-Hours" +msgstr "Horas GB de este mes" + +#: templates/horizon/common/_workflow.html:33 +msgid "Cancel" +msgstr "Cancelar" + +#: templatetags/branding.py:35 +msgid "Horizon" +msgstr "Horizon" + +#: templatetags/horizon.py:109 +msgid "No Limit" +msgstr "Sin límite" + +#: templatetags/horizon.py:111 templatetags/horizon.py:113 +msgid "Available" +msgstr "Disponible" + +#: templatetags/sizeformat.py:45 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "%(size)d byte" +msgstr[1] "%(size)d bytes" + +#: templatetags/sizeformat.py:49 +#, python-format +msgid "%(size)d" +msgid_plural "%(size)d" +msgstr[0] "%(size)d" +msgstr[1] "%(size)d" + +#: templatetags/sizeformat.py:52 +#, python-format +msgid "%s KB" +msgstr "%s KB" + +#: templatetags/sizeformat.py:55 +#, python-format +msgid "%s MB" +msgstr "%s MB" + +#: templatetags/sizeformat.py:58 +#, python-format +msgid "%s GB" +msgstr "%s GB" + +#: templatetags/sizeformat.py:61 +#, python-format +msgid "%s TB" +msgstr "%s TB" + +#: templatetags/sizeformat.py:63 +#, python-format +msgid "%s PB" +msgstr "%s PB" + +#: test/settings.py:114 +msgid "Password must be between 8 and 18 characters." +msgstr "La contraseña debe tener entre 8 y 18 caracteres." + +#: test/test_dashboards/cats/dashboard.py:8 +msgid "Cute Cats" +msgstr "Gatos bonitos" + +#: test/test_dashboards/cats/dashboard.py:14 +msgid "Fierce Cats" +msgstr "Gatos feroces" + +#: test/test_dashboards/cats/dashboard.py:19 +msgid "Cats" +msgstr "Gatos" + +#: test/test_dashboards/cats/kittens/panel.py:9 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:3 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:6 +msgid "Kittens" +msgstr "Gatitos" + +#: test/test_dashboards/cats/tigers/panel.py:9 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:3 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:6 +msgid "Tigers" +msgstr "Tigres" + +#: test/test_dashboards/dogs/dashboard.py:7 +msgid "Dogs" +msgstr "Perros" + +#: test/test_dashboards/dogs/puppies/panel.py:9 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:3 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:6 +msgid "Puppies" +msgstr "Perritos" + +#: test/tests/base.py:39 +msgid "My Dashboard" +msgstr "Mi Dashboard" + +#: test/tests/base.py:45 +msgid "My Panel" +msgstr "Mi Panel" + +#: test/tests/base.py:51 +msgid "Admin Panel" +msgstr "Panel de Administración" + +#: test/tests/messages.py:32 +msgid "Giant ants are attacking San Francisco!" +msgstr "¡Hormigas gigantes están atacando Sevilla!" + +#: test/tests/messages.py:46 +msgid "We are now safe from ants! Go here!" +msgstr "" + +#: test/tests/tables.py:107 +msgid "Batch" +msgstr "" + +#: test/tests/tables.py:108 +msgid "Batched" +msgstr "" + +#: test/tests/tables.py:109 test/tests/tables.py:120 +msgid "Item" +msgstr "Item" + +#: test/tests/tables.py:110 test/tests/tables.py:121 +msgid "Items" +msgstr "Items" + +#: test/tests/tables.py:118 +msgid "Down" +msgstr "Abajo" + +#: test/tests/tables.py:118 +msgid "Up" +msgstr "Arriba" + +#: test/tests/tables.py:119 +msgid "Downed" +msgstr "Bajados" + +#: test/tests/tables.py:119 +msgid "Upped" +msgstr "Subidos" + +#: test/tests/tables.py:187 +msgid "No Actions Table" +msgstr "" + +#: test/tests/tables.py:684 +msgid "Single Table" +msgstr "" + +#: test/tests/tabs.py:36 +msgid "Tab One" +msgstr "" + +#: test/tests/tabs.py:42 +msgid "Delayed Tab" +msgstr "" + +#: test/tests/tabs.py:49 +msgid "Disabled Tab" +msgstr "" + +#: test/tests/tabs.py:58 +msgid "Disallowed Tab" +msgstr "" + +#: test/tests/tabs.py:76 +msgid "Tab With My Table" +msgstr "" + +#: test/tests/tabs.py:85 +msgid "Recoverable Error Tab" +msgstr "" + +#: test/tests/workflows.py:43 +msgid "Project" +msgstr "Proyecto" + +#: test/tests/workflows.py:44 +msgid "User" +msgstr "Usuario" + +#: test/tests/workflows.py:47 +msgid "Test Action One" +msgstr "" + +#: test/tests/workflows.py:61 +msgid "Instance" +msgstr "Instancia" + +#: test/tests/workflows.py:64 +msgid "Test Action Two" +msgstr "" + +#: test/tests/workflows.py:72 +msgid "Test Action Three" +msgstr "" + +#: test/tests/workflows.py:77 +msgid "Admin" +msgstr "Admin" + +#: test/tests/workflows.py:80 +msgid "Admin Action" +msgstr "" + +#: utils/fields.py:46 +msgid "Incorrect format for IP address" +msgstr "Formato incorrecto de dirección IP" + +#: utils/fields.py:47 +msgid "Invalid version for IP address" +msgstr "Versión inválida de dirección IP" + +#: utils/fields.py:48 +msgid "Invalid subnet mask" +msgstr "Máscara de red inválida" + +#: workflows/base.py:71 +msgid "Processing..." +msgstr "Procesando..." + +#: workflows/base.py:467 +#, fuzzy +msgid "All available" +msgstr "Disponible" + +#: workflows/base.py:468 +msgid "Members" +msgstr "" + +#: workflows/base.py:469 +#, fuzzy +msgid "None available." +msgstr "Disponible" + +#: workflows/base.py:470 +msgid "No members." +msgstr "" + +#: workflows/base.py:569 +msgid "Save" +msgstr "Guardar" + +#: workflows/base.py:570 +#, python-format +msgid "%s completed successfully." +msgstr "%s completado correctamente." + +#: workflows/base.py:571 +#, python-format +msgid "%s did not complete." +msgstr "%s no completado." diff --git a/dashboard/horizon/locale/es/LC_MESSAGES/djangojs.mo b/dashboard/horizon/locale/es/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000000000000000000000000000000000000..d7f6e46db307e57cd0670398bbe4a171c800aef4 GIT binary patch literal 420 zcmYL^PfNov9EKG=_2}6{1P{vm&2}Jc*(!@`HjDj7*A4U*w^%#6q$HW>2l4CqSxi;j z3lB-iljP0U$?@lb&7tMga&9@YTv^)mEgQQzuw(1|CsT66R|dj0EV@<3)3 zEonXv)5OVnBb9+zm(?2XcBO_wV&r6L0TacUv4Z#&rKbuj@J!DGV3(xsS&7HvHXOH#Muo z5UpOB(%krqVha7e*VcOB3aD7gbtOuS`p=bdvU;aj4O6MM8Y9vCEVbVoqTi{1A<&tR Iyi4cc8?IM$3jhEB literal 0 HcmV?d00001 diff --git a/dashboard/horizon/locale/es/LC_MESSAGES/djangojs.po b/dashboard/horizon/locale/es/LC_MESSAGES/djangojs.po new file mode 100644 index 00000000..bceb2658 --- /dev/null +++ b/dashboard/horizon/locale/es/LC_MESSAGES/djangojs.po @@ -0,0 +1,74 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:09+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +#: static/horizon/js/horizon.forms.js:47 +msgid "Additional information here..." +msgstr "" + +#: static/horizon/js/horizon.forms.js:53 +msgid "Filter" +msgstr "" + +#: static/horizon/js/horizon.instances.js:28 +msgid "There was a problem communicating with the server, please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:125 +msgid "There was an error submitting the form. Please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:159 static/horizon/js/horizon.tabs.js:9 +msgid "Loading" +msgstr "" + +#: static/horizon/js/horizon.modals.js:178 +msgid "An error occurred. Please try again." +msgstr "" + +#: static/horizon/js/horizon.tables.js:47 +msgid "An error occurred while updating." +msgstr "" + +#: static/horizon/js/horizon.tables.js:145 +msgid "You have selected " +msgstr "" + +#: static/horizon/js/horizon.tables.js:158 +msgid "Confirm " +msgstr "" + +#: static/horizon/js/horizon.tables.js:159 +msgid "Please confirm your selection. This action cannot be undone." +msgstr "" + +#: static/horizon/js/horizon.tables.js:173 +msgid "Working" +msgstr "" + +#: static/horizon/js/horizon.tables.js:216 +#, c-format +msgid "Displaying %s item" +msgid_plural "Displaying %s items" +msgstr[0] "" +msgstr[1] "" + +#: static/horizon/js/horizon.users.js:18 +msgid "Passwords do not match." +msgstr "" diff --git a/dashboard/horizon/locale/fr/LC_MESSAGES/django.mo b/dashboard/horizon/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..df7940361619481e7565aaa121d9b2efc76ecf31 GIT binary patch literal 414 zcmYk1K~KUk6vs7s>d~`@9z1CD;kJZi6d_C)NYWvJ;k~jlYeu`Y#i-xIujgm+O%m~+ z{L9v(v95%rSC~Tp%aN4bmh=3OwxK+BkoiTk|T{9;O96N$Zr+j6N8vK8+>| zSsUvibEPWaepfok4WnfKn8VOY;ZcT>@RHFe2qOps2--C4u&8q#U{HjGd{fP%|Gl6M zL5x>e6zx4OoQK>B?JD7oWpupaoX(OgrFUtTO!*)gjTY&0*(Y4+awkd&xfEMQc`}=< zk_md84=U9%wmdkDZY;^BS$g!bT?wVTbp0Z%lfJjg#}FU9 vX0VO4km|K5lr9;KpOq)`YG*|SV`I0DQC\n" +"Language-Team: LANGUAGE \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: base.py:424 +msgid "Other" +msgstr "" + +#: decorators.py:55 +msgid "Please log in to continue." +msgstr "" + +#: decorators.py:87 +#, python-format +msgid "You are not authorized to access %s" +msgstr "" + +#: exceptions.py:283 +msgid "Unauthorized. Please try logging in again." +msgstr "" + +#: browsers/base.py:90 +msgid "Navigation Item" +msgstr "" + +#: browsers/views.py:42 +#, python-format +msgid "Select a %s to browse." +msgstr "" + +#: conf/default.py:29 +msgid "Password is not accepted" +msgstr "" + +#: tables/actions.py:349 +msgid "Filter" +msgstr "" + +#: tables/actions.py:527 +#, python-format +msgid "%(action)s %(data_type)s" +msgstr "" + +#: tables/actions.py:561 +msgid "N/A" +msgstr "" + +#: tables/actions.py:589 +#, python-format +msgid "You do not have permission to %(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:595 +#, python-format +msgid "Unable to %(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:601 +#, python-format +msgid "%(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:611 +msgid "Delete" +msgstr "" + +#: tables/actions.py:612 +msgid "Deleted" +msgstr "" + +#: tables/base.py:275 +#, python-format +msgid "The attribute %(attr)s doesn't exist on %(obj)s." +msgstr "" + +#: tables/base.py:748 +msgid "No items to display." +msgstr "" + +#: tables/base.py:852 +msgid "Actions" +msgstr "" + +#: tables/base.py:1035 +#, python-format +msgid "No match returned for the id \"%s\"." +msgstr "" + +#: tables/base.py:1165 +msgid "Please select a row before taking that action." +msgstr "" + +#: templates/_header.html:3 +msgid "Logged in as" +msgstr "" + +#: templates/_header.html:5 +msgid "Help" +msgstr "" + +#: templates/_header.html:7 +msgid "Sign Out" +msgstr "" + +#: templates/splash.html:7 templates/auth/login.html:4 +msgid "Login" +msgstr "" + +#: templates/auth/_login.html:4 +msgid "Log In" +msgstr "" + +#: templates/auth/_login.html:14 +msgid "You don't have permissions to access:" +msgstr "" + +#: templates/auth/_login.html:16 +msgid "Login as different user or go back to" +msgstr "" + +#: templates/auth/_login.html:17 +msgid "home page" +msgstr "" + +#: templates/auth/_login.html:27 +msgid "Sign In" +msgstr "" + +#: templates/horizon/_messages.html:7 +msgid "Info: " +msgstr "" + +#: templates/horizon/_messages.html:13 +msgid "Warning: " +msgstr "" + +#: templates/horizon/_messages.html:19 +msgid "Success: " +msgstr "" + +#: templates/horizon/_messages.html:25 +msgid "Error: " +msgstr "" + +#: templates/horizon/common/_data_table.html:54 +msgid "Summary" +msgstr "" + +#: templates/horizon/common/_data_table.html:63 +#, python-format +msgid "Displaying %(counter)s item" +msgid_plural "Displaying %(counter)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_data_table_row_actions.html:10 +msgid "More" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:4 +msgid "Quota Summary" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "Used" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "of" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +msgid "Available Instances" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:8 +msgid "Available vCPUs" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:11 +msgid "Available RAM" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:15 +msgid "Available volumes" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:18 +msgid "Available volume storage" +msgstr "" + +#: templates/horizon/common/_resource_browser.html:10 +#, python-format +msgid "Displaying %(nav_items)s item" +msgid_plural "Displaying %(nav_items)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_resource_browser.html:11 +#, python-format +msgid "Displaying %(content_items)s item" +msgid_plural "Displaying %(content_items)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_sidebar.html:14 +msgid "Current Project" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:5 +msgid "Select a month to query its usage" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:9 +msgid "Submit" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:14 +msgid "Active Instances" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:15 +msgid "Active RAM" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:16 +msgid "This Month's VCPU-Hours" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:17 +msgid "This Month's GB-Hours" +msgstr "" + +#: templates/horizon/common/_workflow.html:33 +msgid "Cancel" +msgstr "" + +#: templatetags/branding.py:35 +msgid "Horizon" +msgstr "" + +#: templatetags/horizon.py:109 +msgid "No Limit" +msgstr "" + +#: templatetags/horizon.py:111 templatetags/horizon.py:113 +msgid "Available" +msgstr "" + +#: templatetags/sizeformat.py:45 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "" +msgstr[1] "" + +#: templatetags/sizeformat.py:49 +#, python-format +msgid "%(size)d" +msgid_plural "%(size)d" +msgstr[0] "" +msgstr[1] "" + +#: templatetags/sizeformat.py:52 +#, python-format +msgid "%s KB" +msgstr "" + +#: templatetags/sizeformat.py:55 +#, python-format +msgid "%s MB" +msgstr "" + +#: templatetags/sizeformat.py:58 +#, python-format +msgid "%s GB" +msgstr "" + +#: templatetags/sizeformat.py:61 +#, python-format +msgid "%s TB" +msgstr "" + +#: templatetags/sizeformat.py:63 +#, python-format +msgid "%s PB" +msgstr "" + +#: test/settings.py:114 +msgid "Password must be between 8 and 18 characters." +msgstr "" + +#: test/test_dashboards/cats/dashboard.py:8 +msgid "Cute Cats" +msgstr "" + +#: test/test_dashboards/cats/dashboard.py:14 +msgid "Fierce Cats" +msgstr "" + +#: test/test_dashboards/cats/dashboard.py:19 +msgid "Cats" +msgstr "" + +#: test/test_dashboards/cats/kittens/panel.py:9 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:3 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:6 +msgid "Kittens" +msgstr "" + +#: test/test_dashboards/cats/tigers/panel.py:9 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:3 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:6 +msgid "Tigers" +msgstr "" + +#: test/test_dashboards/dogs/dashboard.py:7 +msgid "Dogs" +msgstr "" + +#: test/test_dashboards/dogs/puppies/panel.py:9 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:3 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:6 +msgid "Puppies" +msgstr "" + +#: test/tests/base.py:39 +msgid "My Dashboard" +msgstr "" + +#: test/tests/base.py:45 +msgid "My Panel" +msgstr "" + +#: test/tests/base.py:51 +msgid "Admin Panel" +msgstr "" + +#: test/tests/messages.py:32 +msgid "Giant ants are attacking San Francisco!" +msgstr "" + +#: test/tests/messages.py:46 +msgid "We are now safe from ants! Go here!" +msgstr "" + +#: test/tests/tables.py:107 +msgid "Batch" +msgstr "" + +#: test/tests/tables.py:108 +msgid "Batched" +msgstr "" + +#: test/tests/tables.py:109 test/tests/tables.py:120 +msgid "Item" +msgstr "" + +#: test/tests/tables.py:110 test/tests/tables.py:121 +msgid "Items" +msgstr "" + +#: test/tests/tables.py:118 +msgid "Down" +msgstr "" + +#: test/tests/tables.py:118 +msgid "Up" +msgstr "" + +#: test/tests/tables.py:119 +msgid "Downed" +msgstr "" + +#: test/tests/tables.py:119 +msgid "Upped" +msgstr "" + +#: test/tests/tables.py:187 +msgid "No Actions Table" +msgstr "" + +#: test/tests/tables.py:684 +msgid "Single Table" +msgstr "" + +#: test/tests/tabs.py:36 +msgid "Tab One" +msgstr "" + +#: test/tests/tabs.py:42 +msgid "Delayed Tab" +msgstr "" + +#: test/tests/tabs.py:49 +msgid "Disabled Tab" +msgstr "" + +#: test/tests/tabs.py:58 +msgid "Disallowed Tab" +msgstr "" + +#: test/tests/tabs.py:76 +msgid "Tab With My Table" +msgstr "" + +#: test/tests/tabs.py:85 +msgid "Recoverable Error Tab" +msgstr "" + +#: test/tests/workflows.py:43 +msgid "Project" +msgstr "" + +#: test/tests/workflows.py:44 +msgid "User" +msgstr "" + +#: test/tests/workflows.py:47 +msgid "Test Action One" +msgstr "" + +#: test/tests/workflows.py:61 +msgid "Instance" +msgstr "" + +#: test/tests/workflows.py:64 +msgid "Test Action Two" +msgstr "" + +#: test/tests/workflows.py:72 +msgid "Test Action Three" +msgstr "" + +#: test/tests/workflows.py:77 +msgid "Admin" +msgstr "" + +#: test/tests/workflows.py:80 +msgid "Admin Action" +msgstr "" + +#: utils/fields.py:46 +msgid "Incorrect format for IP address" +msgstr "" + +#: utils/fields.py:47 +msgid "Invalid version for IP address" +msgstr "" + +#: utils/fields.py:48 +msgid "Invalid subnet mask" +msgstr "" + +#: workflows/base.py:71 +msgid "Processing..." +msgstr "" + +#: workflows/base.py:467 +msgid "All available" +msgstr "" + +#: workflows/base.py:468 +msgid "Members" +msgstr "" + +#: workflows/base.py:469 +msgid "None available." +msgstr "" + +#: workflows/base.py:470 +msgid "No members." +msgstr "" + +#: workflows/base.py:569 +msgid "Save" +msgstr "" + +#: workflows/base.py:570 +#, python-format +msgid "%s completed successfully." +msgstr "" + +#: workflows/base.py:571 +#, python-format +msgid "%s did not complete." +msgstr "" diff --git a/dashboard/horizon/locale/fr/LC_MESSAGES/djangojs.mo b/dashboard/horizon/locale/fr/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000000000000000000000000000000000000..d25b405243b512eeae50070e456f6671a39abab4 GIT binary patch literal 419 zcmYL^O;5uh9EWH0)T3t)96V^|f3{6bHujR?%4TEV(sfJph8xzJ4jN!aKZsw?&mvXh ze#sLcJiw2y)05AR&5`BIa&9@cTv^)mEtTCI*s*o~lPUSiHwMBjthv&a6c}Y>c25=* zt!cgt)5OVnBb9+zm(><-_oaqHV&r6L0W-y!v4UZVKktE(9O4uLg6>irhuz0Cp-#lKfkH8%Yi6XvXugUfl#rOB5sgV0O-T^sbh(^Z zRg`;Haws_4VHA;Mv1%)iqG(!IJ*morW2?qtOb`F_dag5*!f;`q`)F)$!{5zyQ?p78 z(dLCI&CPgK%%Okq+FH+C0Tmm$twf1Y|EV%gR__(7VJ_89VqG literal 0 HcmV?d00001 diff --git a/dashboard/horizon/locale/fr/LC_MESSAGES/djangojs.po b/dashboard/horizon/locale/fr/LC_MESSAGES/djangojs.po new file mode 100644 index 00000000..559b85f2 --- /dev/null +++ b/dashboard/horizon/locale/fr/LC_MESSAGES/djangojs.po @@ -0,0 +1,74 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:08+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" + +#: static/horizon/js/horizon.forms.js:47 +msgid "Additional information here..." +msgstr "" + +#: static/horizon/js/horizon.forms.js:53 +msgid "Filter" +msgstr "" + +#: static/horizon/js/horizon.instances.js:28 +msgid "There was a problem communicating with the server, please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:125 +msgid "There was an error submitting the form. Please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:159 static/horizon/js/horizon.tabs.js:9 +msgid "Loading" +msgstr "" + +#: static/horizon/js/horizon.modals.js:178 +msgid "An error occurred. Please try again." +msgstr "" + +#: static/horizon/js/horizon.tables.js:47 +msgid "An error occurred while updating." +msgstr "" + +#: static/horizon/js/horizon.tables.js:145 +msgid "You have selected " +msgstr "" + +#: static/horizon/js/horizon.tables.js:158 +msgid "Confirm " +msgstr "" + +#: static/horizon/js/horizon.tables.js:159 +msgid "Please confirm your selection. This action cannot be undone." +msgstr "" + +#: static/horizon/js/horizon.tables.js:173 +msgid "Working" +msgstr "" + +#: static/horizon/js/horizon.tables.js:216 +#, c-format +msgid "Displaying %s item" +msgid_plural "Displaying %s items" +msgstr[0] "" +msgstr[1] "" + +#: static/horizon/js/horizon.users.js:18 +msgid "Passwords do not match." +msgstr "" diff --git a/dashboard/horizon/locale/it/LC_MESSAGES/django.mo b/dashboard/horizon/locale/it/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..5fbf5df568854166a9aa66a1a3bdd1ef53fb939f GIT binary patch literal 2838 zcmZvdPi!Pd6^AQfVZnifEQCLSKzW1ldPxtqwHs~5>nOvnZ7l4KS$jbUBvkG$w`RYT#aQC{B3Jd0Qfu(dhxuW=_93O7gujNLhQEjB;oI<&@E!PJ_)qu|_#Qk5 z{{DSe*Q5w6Z@-B^u7+I4t@Z| zkJq5Y^&9vEd<%}?-=M_v{dX%a5!ZK}^(E&u_!aJbxd``=7zDz+XT)|94R8 z;15vr`ZK%&{{fLveU(dwip)WmGjgLmAdKcs5V0IVW`!nv!U$mHeHi!P+QKh z4Oi4ASZz`=qlW0Rq`_q3_1Kv=w=Im^w7X$UQL zhpkO|Y~oAr&z;y1iBPt9aP7BTN|v~9dF_%NN1Z(C#7SdNib;+uk9DxA3oS~@>U3;1 zhg@8U7YFJ^o`TvlF~00WrCrRr8uXr^yxME5iB>nh)~=PkOW!8fc6M<3Dxw*Y86Et~ z`p|NK)yZTT(lN1`oEXf5mmDI`V341(_IurA;>gmzIk9SgmS)7>@5W;j?yCb_Mb_XB zB}WUF`<>RudEg;%f<3lz4F{}^|`MtGcYV}+Y=|}KWMjntysOHSFQ|KS7&xRCZ^(mvW$&M zQqwy)5cPfvemZrn)7P7LdjCQ_Hm(`)2|ho!W7@hiwJi>;8MA71YwONvYeQe(*?FOH z10U)$n;}&=x4XT&aTeyvV0rEaRl_G&9NeD}MzZ(P*Qrp~uIcjFglNz7(Rzphu7*5;&5^c{p~x#bJw z10CZA#mmcWo$#`sAnvhXjRHBot)@<7E&c?HO zq%2Kfnc8R^%grg1mqx_2^b|YH)|8dggz8I2k7-DN+GlkzM|h)aSJPjd7N=TjP+ClB zTB`4snfS)dE73v%bh9QS-cXOaS#)jXD6pxgUGz~IwQFL@2XA!rSfYr{I^?!v2CA=3 zt1H`laEAxX)UGQfxLbS{M;};EOQu)JrqShp34}#nvs7d0#96cd+pYcwdUVa@ literal 0 HcmV?d00001 diff --git a/dashboard/horizon/locale/it/LC_MESSAGES/django.po b/dashboard/horizon/locale/it/LC_MESSAGES/django.po new file mode 100644 index 00000000..1a99c065 --- /dev/null +++ b/dashboard/horizon/locale/it/LC_MESSAGES/django.po @@ -0,0 +1,520 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Loris Strozzini , 2012. +# Salvatore Orlando , 2012. +# , 2012. +msgid "" +msgstr "" +"Project-Id-Version: Horizon\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:08+0000\n" +"PO-Revision-Date: 2012-08-24 14:22+0000\n" +"Last-Translator: Loris Strozzini \n" +"Language-Team: LANGUAGE \n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: base.py:424 +msgid "Other" +msgstr "Altro" + +#: decorators.py:55 +msgid "Please log in to continue." +msgstr "Accedi per continuare" + +#: decorators.py:87 +#, python-format +msgid "You are not authorized to access %s" +msgstr "Accesso non autorizzato a %s" + +#: exceptions.py:283 +msgid "Unauthorized. Please try logging in again." +msgstr "Non autorizzato. Ritentare il login." + +#: browsers/base.py:90 +msgid "Navigation Item" +msgstr "" + +#: browsers/views.py:42 +#, python-format +msgid "Select a %s to browse." +msgstr "" + +#: conf/default.py:29 +msgid "Password is not accepted" +msgstr "La password non è stata accettata." + +#: tables/actions.py:349 +msgid "Filter" +msgstr "Filtro" + +#: tables/actions.py:527 +#, python-format +msgid "%(action)s %(data_type)s" +msgstr "" + +#: tables/actions.py:561 +msgid "N/A" +msgstr "" + +#: tables/actions.py:589 +#, python-format +msgid "You do not have permission to %(action)s: %(objs)s" +msgstr "Non si dispone dei permessi per %(action)s: %(objs)s" + +#: tables/actions.py:595 +#, python-format +msgid "Unable to %(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:601 +#, python-format +msgid "%(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:611 +msgid "Delete" +msgstr "Elimina" + +#: tables/actions.py:612 +msgid "Deleted" +msgstr "Eliminato" + +#: tables/base.py:275 +#, python-format +msgid "The attribute %(attr)s doesn't exist on %(obj)s." +msgstr "L'attributo %(attr)s non esiste in %(obj)s" + +#: tables/base.py:748 +msgid "No items to display." +msgstr "Nessun elemento da visualizzare" + +#: tables/base.py:852 +msgid "Actions" +msgstr "Azioni" + +#: tables/base.py:1035 +#, python-format +msgid "No match returned for the id \"%s\"." +msgstr "Nessuna corrispondenza restituita per l'identificativo \"%s\"" + +#: tables/base.py:1165 +msgid "Please select a row before taking that action." +msgstr "Per cortesia, selezionare una riga prima di eseguire tale azione." + +#: templates/_header.html:3 +msgid "Logged in as" +msgstr "" + +#: templates/_header.html:5 +msgid "Help" +msgstr "" + +#: templates/_header.html:7 +msgid "Sign Out" +msgstr "" + +#: templates/splash.html:7 templates/auth/login.html:4 +msgid "Login" +msgstr "" + +#: templates/auth/_login.html:4 +msgid "Log In" +msgstr "" + +#: templates/auth/_login.html:14 +#, fuzzy +msgid "You don't have permissions to access:" +msgstr "Non si dispone dei permessi per %(action)s: %(objs)s" + +#: templates/auth/_login.html:16 +msgid "Login as different user or go back to" +msgstr "" + +#: templates/auth/_login.html:17 +msgid "home page" +msgstr "" + +#: templates/auth/_login.html:27 +msgid "Sign In" +msgstr "" + +#: templates/horizon/_messages.html:7 +msgid "Info: " +msgstr "" + +#: templates/horizon/_messages.html:13 +msgid "Warning: " +msgstr "Attenzione:" + +#: templates/horizon/_messages.html:19 +msgid "Success: " +msgstr "Successo:" + +#: templates/horizon/_messages.html:25 +msgid "Error: " +msgstr "Errore:" + +#: templates/horizon/common/_data_table.html:54 +msgid "Summary" +msgstr "Riepilogo" + +#: templates/horizon/common/_data_table.html:63 +#, python-format +msgid "Displaying %(counter)s item" +msgid_plural "Displaying %(counter)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_data_table_row_actions.html:10 +msgid "More" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:4 +#, fuzzy +msgid "Quota Summary" +msgstr "Riepilogo" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "Used" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "of" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#, fuzzy +msgid "Available Instances" +msgstr "Istanze attive" + +#: templates/horizon/common/_quota_summary.html:8 +msgid "Available vCPUs" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:11 +#, fuzzy +msgid "Available RAM" +msgstr "Non disponibile" + +#: templates/horizon/common/_quota_summary.html:15 +#, fuzzy +msgid "Available volumes" +msgstr "Istanze attive" + +#: templates/horizon/common/_quota_summary.html:18 +#, fuzzy +msgid "Available volume storage" +msgstr "Istanze attive" + +#: templates/horizon/common/_resource_browser.html:10 +#, python-format +msgid "Displaying %(nav_items)s item" +msgid_plural "Displaying %(nav_items)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_resource_browser.html:11 +#, python-format +msgid "Displaying %(content_items)s item" +msgid_plural "Displaying %(content_items)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_sidebar.html:14 +msgid "Current Project" +msgstr "Progetto corrente" + +#: templates/horizon/common/_usage_summary.html:5 +msgid "Select a month to query its usage" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:9 +msgid "Submit" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:14 +msgid "Active Instances" +msgstr "Istanze attive" + +#: templates/horizon/common/_usage_summary.html:15 +msgid "Active RAM" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:16 +msgid "This Month's VCPU-Hours" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:17 +msgid "This Month's GB-Hours" +msgstr "" + +#: templates/horizon/common/_workflow.html:33 +msgid "Cancel" +msgstr "Annulla" + +#: templatetags/branding.py:35 +msgid "Horizon" +msgstr "" + +#: templatetags/horizon.py:109 +msgid "No Limit" +msgstr "" + +#: templatetags/horizon.py:111 templatetags/horizon.py:113 +msgid "Available" +msgstr "" + +#: templatetags/sizeformat.py:45 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "" +msgstr[1] "" + +#: templatetags/sizeformat.py:49 +#, python-format +msgid "%(size)d" +msgid_plural "%(size)d" +msgstr[0] "" +msgstr[1] "" + +#: templatetags/sizeformat.py:52 +#, python-format +msgid "%s KB" +msgstr "" + +#: templatetags/sizeformat.py:55 +#, python-format +msgid "%s MB" +msgstr "" + +#: templatetags/sizeformat.py:58 +#, python-format +msgid "%s GB" +msgstr "" + +#: templatetags/sizeformat.py:61 +#, python-format +msgid "%s TB" +msgstr "" + +#: templatetags/sizeformat.py:63 +#, python-format +msgid "%s PB" +msgstr "" + +#: test/settings.py:114 +msgid "Password must be between 8 and 18 characters." +msgstr "" + +#: test/test_dashboards/cats/dashboard.py:8 +msgid "Cute Cats" +msgstr "Gattini carini" + +#: test/test_dashboards/cats/dashboard.py:14 +msgid "Fierce Cats" +msgstr "Gattacci feroci" + +#: test/test_dashboards/cats/dashboard.py:19 +msgid "Cats" +msgstr "Gatti" + +#: test/test_dashboards/cats/kittens/panel.py:9 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:3 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:6 +msgid "Kittens" +msgstr "Micini" + +#: test/test_dashboards/cats/tigers/panel.py:9 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:3 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:6 +msgid "Tigers" +msgstr "Tigri" + +#: test/test_dashboards/dogs/dashboard.py:7 +msgid "Dogs" +msgstr "Cani" + +#: test/test_dashboards/dogs/puppies/panel.py:9 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:3 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:6 +msgid "Puppies" +msgstr "Cucciolini" + +#: test/tests/base.py:39 +msgid "My Dashboard" +msgstr "" + +#: test/tests/base.py:45 +msgid "My Panel" +msgstr "" + +#: test/tests/base.py:51 +msgid "Admin Panel" +msgstr "" + +#: test/tests/messages.py:32 +msgid "Giant ants are attacking San Francisco!" +msgstr "Scarrafoni giganti stanno attaccando Napoli!" + +#: test/tests/messages.py:46 +msgid "We are now safe from ants! Go here!" +msgstr "" + +#: test/tests/tables.py:107 +msgid "Batch" +msgstr "" + +#: test/tests/tables.py:108 +msgid "Batched" +msgstr "" + +#: test/tests/tables.py:109 test/tests/tables.py:120 +msgid "Item" +msgstr "" + +#: test/tests/tables.py:110 test/tests/tables.py:121 +msgid "Items" +msgstr "" + +#: test/tests/tables.py:118 +msgid "Down" +msgstr "" + +#: test/tests/tables.py:118 +msgid "Up" +msgstr "" + +#: test/tests/tables.py:119 +msgid "Downed" +msgstr "" + +#: test/tests/tables.py:119 +msgid "Upped" +msgstr "" + +#: test/tests/tables.py:187 +msgid "No Actions Table" +msgstr "" + +#: test/tests/tables.py:684 +msgid "Single Table" +msgstr "" + +#: test/tests/tabs.py:36 +msgid "Tab One" +msgstr "" + +#: test/tests/tabs.py:42 +msgid "Delayed Tab" +msgstr "" + +#: test/tests/tabs.py:49 +msgid "Disabled Tab" +msgstr "" + +#: test/tests/tabs.py:58 +msgid "Disallowed Tab" +msgstr "" + +#: test/tests/tabs.py:76 +msgid "Tab With My Table" +msgstr "" + +#: test/tests/tabs.py:85 +msgid "Recoverable Error Tab" +msgstr "" + +#: test/tests/workflows.py:43 +msgid "Project" +msgstr "Progetto" + +#: test/tests/workflows.py:44 +msgid "User" +msgstr "Utente" + +#: test/tests/workflows.py:47 +msgid "Test Action One" +msgstr "" + +#: test/tests/workflows.py:61 +msgid "Instance" +msgstr "Istanza" + +#: test/tests/workflows.py:64 +msgid "Test Action Two" +msgstr "" + +#: test/tests/workflows.py:72 +msgid "Test Action Three" +msgstr "" + +#: test/tests/workflows.py:77 +msgid "Admin" +msgstr "Amministratore" + +#: test/tests/workflows.py:80 +msgid "Admin Action" +msgstr "" + +#: utils/fields.py:46 +msgid "Incorrect format for IP address" +msgstr "Formato incorretto per l'indirizzo IP" + +#: utils/fields.py:47 +msgid "Invalid version for IP address" +msgstr "Versione dell'indirizzo IP non valida" + +#: utils/fields.py:48 +msgid "Invalid subnet mask" +msgstr "Maschera sottorete non valida." + +#: workflows/base.py:71 +msgid "Processing..." +msgstr "Attendere prego..." + +#: workflows/base.py:467 +#, fuzzy +msgid "All available" +msgstr "Non disponibile" + +#: workflows/base.py:468 +msgid "Members" +msgstr "" + +#: workflows/base.py:469 +msgid "None available." +msgstr "" + +#: workflows/base.py:470 +msgid "No members." +msgstr "" + +#: workflows/base.py:569 +msgid "Save" +msgstr "Salva" + +#: workflows/base.py:570 +#, python-format +msgid "%s completed successfully." +msgstr "%s completato correttamente." + +#: workflows/base.py:571 +#, python-format +msgid "%s did not complete." +msgstr "%s non completato." diff --git a/dashboard/horizon/locale/it/LC_MESSAGES/djangojs.mo b/dashboard/horizon/locale/it/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000000000000000000000000000000000000..d7f6e46db307e57cd0670398bbe4a171c800aef4 GIT binary patch literal 420 zcmYL^PfNov9EKG=_2}6{1P{vm&2}Jc*(!@`HjDj7*A4U*w^%#6q$HW>2l4CqSxi;j z3lB-iljP0U$?@lb&7tMga&9@YTv^)mEgQQzuw(1|CsT66R|dj0EV@<3)3 zEonXv)5OVnBb9+zm(?2XcBO_wV&r6L0TacUv4Z#&rKbuj@J!DGV3(xsS&7HvHXOH#Muo z5UpOB(%krqVha7e*VcOB3aD7gbtOuS`p=bdvU;aj4O6MM8Y9vCEVbVoqTi{1A<&tR Iyi4cc8?IM$3jhEB literal 0 HcmV?d00001 diff --git a/dashboard/horizon/locale/it/LC_MESSAGES/djangojs.po b/dashboard/horizon/locale/it/LC_MESSAGES/djangojs.po new file mode 100644 index 00000000..bceb2658 --- /dev/null +++ b/dashboard/horizon/locale/it/LC_MESSAGES/djangojs.po @@ -0,0 +1,74 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:09+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +#: static/horizon/js/horizon.forms.js:47 +msgid "Additional information here..." +msgstr "" + +#: static/horizon/js/horizon.forms.js:53 +msgid "Filter" +msgstr "" + +#: static/horizon/js/horizon.instances.js:28 +msgid "There was a problem communicating with the server, please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:125 +msgid "There was an error submitting the form. Please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:159 static/horizon/js/horizon.tabs.js:9 +msgid "Loading" +msgstr "" + +#: static/horizon/js/horizon.modals.js:178 +msgid "An error occurred. Please try again." +msgstr "" + +#: static/horizon/js/horizon.tables.js:47 +msgid "An error occurred while updating." +msgstr "" + +#: static/horizon/js/horizon.tables.js:145 +msgid "You have selected " +msgstr "" + +#: static/horizon/js/horizon.tables.js:158 +msgid "Confirm " +msgstr "" + +#: static/horizon/js/horizon.tables.js:159 +msgid "Please confirm your selection. This action cannot be undone." +msgstr "" + +#: static/horizon/js/horizon.tables.js:173 +msgid "Working" +msgstr "" + +#: static/horizon/js/horizon.tables.js:216 +#, c-format +msgid "Displaying %s item" +msgid_plural "Displaying %s items" +msgstr[0] "" +msgstr[1] "" + +#: static/horizon/js/horizon.users.js:18 +msgid "Passwords do not match." +msgstr "" diff --git a/dashboard/horizon/locale/ja/LC_MESSAGES/django.mo b/dashboard/horizon/locale/ja/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..0e9d2ad051ab6445533e470ed782e5d91da33483 GIT binary patch literal 6233 zcmai$Yj9N69l%f3RvWGLt+hU`S{|w0&BJO#@D&J;$RiN~Xluor-Ai(Dvv+&%-NZQ4 z%I=Cl0>jfl6nRM@2niw-BLQ)yGkoxiIvv{&wVifuc9V2E;|DvVowhUm{qNpQmH>KZ z&;8wV&pH3|KmXU=zus`gtBUJc+8wlOE?4R$_}_1HsZnFM%DDx&mSr>vL|3P>yoB?a$lduYQ!-wHVP~=`ylIQhM3zYO~SgCeg4WpJIk5sDpdg<{t!P{ymE*u4hc3ztIKrvr+AUxFdH1B#!{!7%(g z{2siH%|)NP0#1fE(+@+@yB6L8>)<``IVkhfQ1$*koP|)%{}h}8jiA2)%6VUdqSr|%dY%j3e+WbLb5Pdbg!5$mJ@8Rj z2Sxr`DD$?!+3*7>cDNGb&4N`>*8c{I-d#}E?Si6zFBH8$gpwbhKsk>DzpT3-ihfm4 z=FNp-hx(xZRM1}z@~e{xNw)zIWzVpEA1%&!*E=F{XN4Ao-VBw8g+YO+R~Ln~ZQ2M=-% z@$q9ci8;CCjD_nd9;#_ef)^ZJ{gfttmYleg_9U&ACfCwD{JJ?gc$Qiq@gcsJtDYu# zFW2KV@lD|p{}2vim-wgnl6aa;6F*V5RVhtk>gTkDH1U^QWqJ5_iQQ>5iIrc_E~AM* ze?luS&XuMs-(T?sg=M zQm1I3nh~pMrQ^SJ%omN)D5Db|w5AT%7<=vakl|=^X2^Y>e_!C=8~l5te~(zLiJ0LU zQO!w4B8KCvNXB9vVObJ2qgvc@wZfFJs__+de6|@{UEFc?IEz$GwAGB~?^=FR&}q=) zMoiVT>1IrCiWzE#?nYYjcO$B1%IX+!9W^s)+eX~g8f*pa|2v&|UFs5z#N)o6~U+lHpQt{!<_ z4!u~9YqM=EY&sF^9yQm9CDdHYHea;jsxBU}Fn7e&R#u-0-=~L>ey3d!y07NcjqopHb9rY_Sd26u)V=R3pA1Xl=7e5}snnGZ_}O3@1Lt z)r=QRB;tL)0+wRp#ui+(Kn^iQid$%|m9(An-!7fmu%z(bY{oHaN!<ryei2MOBsI zq}w9NVMN1P;6xYe;K*i4CJEMN-HeCTk_5j5fw9CPY?kSEoN;XPw3YM|P%7rQFtM|Q zl0$KK{5es}kFOS7r6mlz)x^gf?^2otx@%NxmE{$cp^EZQ`GZ=;wCc)f_m^`qF|^2NGiCqcdF551 z%7?V_%Ib<>-hAD0LyeRYC#JhnhZ?O`t0Vcmsm-fttk)iP={`>3&|AY%y(4Zmx47Yz ziAPK3>+$9!krirWs9JkMPw+J$JEpd{Zlb!Zti8QG?21@(h4Dg|&rDe&U;Lf2M46SK zWJoP5iv?oJR_bM?V?wL7m3qm7x&^bw?NSjgFPWJy^r1$+q1Bpeyx^8m`>MCJlTJRVTWc>5~IE=tC~!=q#>5H^;l>&<7 z-uLgjy;W%u!KCxYUiZ>lz0|?+NnU!Nmp7`!v((Ap{ZvPP7D!Z<8v}b=|yPgSsthQSXzq~hp#EEN%52SK^`zF4<@K40(p4G#z z_oL{@-rnK;r-lalaC*cl{1M2m>S8RIl-`n`t5=uz$#t_dtlqczCBK>?7O|${RF{VBfAF)5ik8a zu{)DQ06u@$_dgF?a@z)TTQU>${QR^yafisuY#;j@F*LA&fJA8a%)abt+|n=q03=2- z>%7bnM5<( zIc#dXD#TBZm+mUW$;1^>1L|u1KQ3(;S^mM$T?4gfjyhEn4LPxKO{e0*U6uxT=$x6AFByL z&e)YbyL#k6>ii@3i1x!LU-N|%=~#rVCwPd|AxKb zB;E9u(bPb$b5|j-&zF*2bATd82FstVi_{&7Cy4rJlej#3C*o8tlxUm%n#ft|vX(y>B5&h3~DHx)21!8xar)FZvLVytQGZ_jDl<#tO2Z4y!N;$wD}+%AUfg z, 2012. +# Tomoyuki KATO , 2012. +msgid "" +msgstr "" +"Project-Id-Version: Horizon\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:08+0000\n" +"PO-Revision-Date: 2012-08-26 02:19+0000\n" +"Last-Translator: Tomoyuki KATO \n" +"Language-Team: Japanese (http://www.transifex.com/projects/p/openstack/" +"language/ja/)\n" +"Language: ja\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: base.py:424 +msgid "Other" +msgstr "その他" + +#: decorators.py:55 +msgid "Please log in to continue." +msgstr "続けるにはログインしてください。" + +#: decorators.py:87 +#, python-format +msgid "You are not authorized to access %s" +msgstr "%s へのアクセスは許可されていません。" + +#: exceptions.py:283 +msgid "Unauthorized. Please try logging in again." +msgstr "認証されていません。もう一度ログインしてください。" + +#: browsers/base.py:90 +msgid "Navigation Item" +msgstr "ナビゲーション項目" + +#: browsers/views.py:42 +#, python-format +msgid "Select a %s to browse." +msgstr "表示する %s を選択してください。" + +#: conf/default.py:29 +msgid "Password is not accepted" +msgstr "パスワードを受け付けられません" + +#: tables/actions.py:349 +msgid "Filter" +msgstr "フィルター" + +#: tables/actions.py:527 +#, python-format +msgid "%(action)s %(data_type)s" +msgstr "%(data_type)s の %(action)s" + +#: tables/actions.py:561 +msgid "N/A" +msgstr "N/A" + +#: tables/actions.py:589 +#, python-format +msgid "You do not have permission to %(action)s: %(objs)s" +msgstr "%(action)s を実行する権限がありません: %(objs)s" + +#: tables/actions.py:595 +#, python-format +msgid "Unable to %(action)s: %(objs)s" +msgstr "%(action)s を実行できません: %(objs)s" + +#: tables/actions.py:601 +#, python-format +msgid "%(action)s: %(objs)s" +msgstr "%(action)s: %(objs)s" + +#: tables/actions.py:611 +msgid "Delete" +msgstr "削除します" + +#: tables/actions.py:612 +msgid "Deleted" +msgstr "削除しました" + +#: tables/base.py:275 +#, python-format +msgid "The attribute %(attr)s doesn't exist on %(obj)s." +msgstr "属性 %(attr)s が %(obj)s. に存在しません。" + +#: tables/base.py:748 +msgid "No items to display." +msgstr "表示する項目がありません。" + +#: tables/base.py:852 +msgid "Actions" +msgstr "アクション" + +#: tables/base.py:1035 +#, python-format +msgid "No match returned for the id \"%s\"." +msgstr "ID \"%s\" に一致するものが返されませんでした。" + +#: tables/base.py:1165 +msgid "Please select a row before taking that action." +msgstr "アクションを実行する前に行を選択してください。" + +#: templates/_header.html:3 +msgid "Logged in as" +msgstr "次の役割でログインしました" + +#: templates/_header.html:5 +msgid "Help" +msgstr "ヘルプ" + +#: templates/_header.html:7 +msgid "Sign Out" +msgstr "ログアウト" + +#: templates/splash.html:7 templates/auth/login.html:4 +msgid "Login" +msgstr "ログイン" + +#: templates/auth/_login.html:4 +msgid "Log In" +msgstr "ログイン" + +#: templates/auth/_login.html:14 +#, fuzzy +msgid "You don't have permissions to access:" +msgstr "%(action)s を実行する権限がありません: %(objs)s" + +#: templates/auth/_login.html:16 +msgid "Login as different user or go back to" +msgstr "" + +#: templates/auth/_login.html:17 +msgid "home page" +msgstr "" + +#: templates/auth/_login.html:27 +msgid "Sign In" +msgstr "ログイン" + +#: templates/horizon/_messages.html:7 +msgid "Info: " +msgstr "情報: " + +#: templates/horizon/_messages.html:13 +msgid "Warning: " +msgstr "警告: " + +#: templates/horizon/_messages.html:19 +msgid "Success: " +msgstr "成功: " + +#: templates/horizon/_messages.html:25 +msgid "Error: " +msgstr "エラー: " + +#: templates/horizon/common/_data_table.html:54 +msgid "Summary" +msgstr "概要" + +#: templates/horizon/common/_data_table.html:63 +#, python-format +msgid "Displaying %(counter)s item" +msgid_plural "Displaying %(counter)s items" +msgstr[0] "%(counter)s 個の項目を表示しています" + +#: templates/horizon/common/_data_table_row_actions.html:10 +msgid "More" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:4 +#, fuzzy +msgid "Quota Summary" +msgstr "概要" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "Used" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "of" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#, fuzzy +msgid "Available Instances" +msgstr "すべてのインスタンス" + +#: templates/horizon/common/_quota_summary.html:8 +#, fuzzy +msgid "Available vCPUs" +msgstr "利用可能" + +#: templates/horizon/common/_quota_summary.html:11 +#, fuzzy +msgid "Available RAM" +msgstr "利用可能" + +#: templates/horizon/common/_quota_summary.html:15 +#, fuzzy +msgid "Available volumes" +msgstr "利用可能" + +#: templates/horizon/common/_quota_summary.html:18 +#, fuzzy +msgid "Available volume storage" +msgstr "すべてのインスタンス" + +#: templates/horizon/common/_resource_browser.html:10 +#, python-format +msgid "Displaying %(nav_items)s item" +msgid_plural "Displaying %(nav_items)s items" +msgstr[0] "%(nav_items)s 項目の表示中" + +#: templates/horizon/common/_resource_browser.html:11 +#, python-format +msgid "Displaying %(content_items)s item" +msgid_plural "Displaying %(content_items)s items" +msgstr[0] "%(content_items)s 項目の表示中" + +#: templates/horizon/common/_sidebar.html:14 +msgid "Current Project" +msgstr "現在のプロジェクト" + +#: templates/horizon/common/_usage_summary.html:5 +msgid "Select a month to query its usage" +msgstr "使用量を問い合わせる月の選択" + +#: templates/horizon/common/_usage_summary.html:9 +msgid "Submit" +msgstr "送信" + +#: templates/horizon/common/_usage_summary.html:14 +msgid "Active Instances" +msgstr "インスタンスの有効化" + +#: templates/horizon/common/_usage_summary.html:15 +#, fuzzy +msgid "Active RAM" +msgstr "メモリーの有効化" + +#: templates/horizon/common/_usage_summary.html:16 +msgid "This Month's VCPU-Hours" +msgstr "今月の仮想 CPU 時間" + +#: templates/horizon/common/_usage_summary.html:17 +msgid "This Month's GB-Hours" +msgstr "今月の GB 時間" + +#: templates/horizon/common/_workflow.html:33 +msgid "Cancel" +msgstr "キャンセル" + +#: templatetags/branding.py:35 +msgid "Horizon" +msgstr "Horizon" + +#: templatetags/horizon.py:109 +msgid "No Limit" +msgstr "制限なし" + +#: templatetags/horizon.py:111 templatetags/horizon.py:113 +msgid "Available" +msgstr "利用可能" + +#: templatetags/sizeformat.py:45 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "%(size)d バイト" + +#: templatetags/sizeformat.py:49 +#, python-format +msgid "%(size)d" +msgid_plural "%(size)d" +msgstr[0] "%(size)d" + +#: templatetags/sizeformat.py:52 +#, python-format +msgid "%s KB" +msgstr "%s KB" + +#: templatetags/sizeformat.py:55 +#, python-format +msgid "%s MB" +msgstr "%s MB" + +#: templatetags/sizeformat.py:58 +#, python-format +msgid "%s GB" +msgstr "%s GB" + +#: templatetags/sizeformat.py:61 +#, python-format +msgid "%s TB" +msgstr "%s TB" + +#: templatetags/sizeformat.py:63 +#, python-format +msgid "%s PB" +msgstr "%s PB" + +#: test/settings.py:114 +msgid "Password must be between 8 and 18 characters." +msgstr "パスワードは 8 から 18 文字である必要があります。" + +#: test/test_dashboards/cats/dashboard.py:8 +msgid "Cute Cats" +msgstr "かわいいネコ" + +#: test/test_dashboards/cats/dashboard.py:14 +msgid "Fierce Cats" +msgstr "獰猛なネコ" + +#: test/test_dashboards/cats/dashboard.py:19 +msgid "Cats" +msgstr "ネコ" + +#: test/test_dashboards/cats/kittens/panel.py:9 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:3 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:6 +msgid "Kittens" +msgstr "子ネコ" + +#: test/test_dashboards/cats/tigers/panel.py:9 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:3 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:6 +msgid "Tigers" +msgstr "トラ" + +#: test/test_dashboards/dogs/dashboard.py:7 +msgid "Dogs" +msgstr "イヌ" + +#: test/test_dashboards/dogs/puppies/panel.py:9 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:3 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:6 +msgid "Puppies" +msgstr "子イヌ" + +#: test/tests/base.py:39 +msgid "My Dashboard" +msgstr "マイダッシュボード" + +#: test/tests/base.py:45 +msgid "My Panel" +msgstr "マイパネル" + +#: test/tests/base.py:51 +msgid "Admin Panel" +msgstr "管理パネル" + +#: test/tests/messages.py:32 +msgid "Giant ants are attacking San Francisco!" +msgstr "巨大な蟻がサンフランシスコを攻撃しています!" + +#: test/tests/messages.py:46 +msgid "We are now safe from ants! Go here!" +msgstr "" + +#: test/tests/tables.py:107 +msgid "Batch" +msgstr "バッチ" + +#: test/tests/tables.py:108 +msgid "Batched" +msgstr "バッチしました" + +#: test/tests/tables.py:109 test/tests/tables.py:120 +msgid "Item" +msgstr "項目" + +#: test/tests/tables.py:110 test/tests/tables.py:121 +msgid "Items" +msgstr "項目" + +#: test/tests/tables.py:118 +msgid "Down" +msgstr "下" + +#: test/tests/tables.py:118 +msgid "Up" +msgstr "上" + +#: test/tests/tables.py:119 +msgid "Downed" +msgstr "下げました" + +#: test/tests/tables.py:119 +msgid "Upped" +msgstr "上げました" + +#: test/tests/tables.py:187 +msgid "No Actions Table" +msgstr "アクションテーブルがありません" + +#: test/tests/tables.py:684 +msgid "Single Table" +msgstr "" + +#: test/tests/tabs.py:36 +msgid "Tab One" +msgstr "タブ 1" + +#: test/tests/tabs.py:42 +msgid "Delayed Tab" +msgstr "遅延されたタブ" + +#: test/tests/tabs.py:49 +msgid "Disabled Tab" +msgstr "無効化されたタブ" + +#: test/tests/tabs.py:58 +msgid "Disallowed Tab" +msgstr "禁止されたタブ" + +#: test/tests/tabs.py:76 +msgid "Tab With My Table" +msgstr "テーブルのあるタブ" + +#: test/tests/tabs.py:85 +msgid "Recoverable Error Tab" +msgstr "修復可能なエラーのタブ" + +#: test/tests/workflows.py:43 +msgid "Project" +msgstr "プロジェクト" + +#: test/tests/workflows.py:44 +msgid "User" +msgstr "ユーザー" + +#: test/tests/workflows.py:47 +msgid "Test Action One" +msgstr "テストアクション 1" + +#: test/tests/workflows.py:61 +msgid "Instance" +msgstr "インスタンス" + +#: test/tests/workflows.py:64 +msgid "Test Action Two" +msgstr "テストアクション 2" + +#: test/tests/workflows.py:72 +msgid "Test Action Three" +msgstr "テストアクション 3" + +#: test/tests/workflows.py:77 +msgid "Admin" +msgstr "管理" + +#: test/tests/workflows.py:80 +msgid "Admin Action" +msgstr "管理アクション" + +#: utils/fields.py:46 +msgid "Incorrect format for IP address" +msgstr "不正な形式の IP アドレス" + +#: utils/fields.py:47 +msgid "Invalid version for IP address" +msgstr "IP アドレスの無効なバージョン" + +#: utils/fields.py:48 +msgid "Invalid subnet mask" +msgstr "無効なサブネットマスク" + +#: workflows/base.py:71 +msgid "Processing..." +msgstr "処理中..." + +#: workflows/base.py:467 +#, fuzzy +msgid "All available" +msgstr "利用可能" + +#: workflows/base.py:468 +msgid "Members" +msgstr "" + +#: workflows/base.py:469 +#, fuzzy +msgid "None available." +msgstr "利用可能" + +#: workflows/base.py:470 +msgid "No members." +msgstr "" + +#: workflows/base.py:569 +msgid "Save" +msgstr "保存" + +#: workflows/base.py:570 +#, python-format +msgid "%s completed successfully." +msgstr "%s が正常に完了しました。" + +#: workflows/base.py:571 +#, python-format +msgid "%s did not complete." +msgstr "%s が完了しませんでした。" diff --git a/dashboard/horizon/locale/ja/LC_MESSAGES/djangojs.mo b/dashboard/horizon/locale/ja/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000000000000000000000000000000000000..56ad7ecd41fd04ff1ed387d0890e5ee204ecaa53 GIT binary patch literal 413 zcmYL^Pfx-y7>6->+R?Lz9=zd;8;J=NFici(asM&~iQej{b7rw1(1Ke$1Oc~#lA-Gj!E?dABHow|$9)%rBIM$& z?k@lH?wOxMoI>D}+myy({~=A76LDjq&|I2^Tcs&kZlZ`Je$2>}#eNvg{UFHMdVP(m ztoFPTPze4?N#rN1O;>puMf0W}C|w;KtQv, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:09+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0\n" + +#: static/horizon/js/horizon.forms.js:47 +msgid "Additional information here..." +msgstr "" + +#: static/horizon/js/horizon.forms.js:53 +msgid "Filter" +msgstr "" + +#: static/horizon/js/horizon.instances.js:28 +msgid "There was a problem communicating with the server, please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:125 +msgid "There was an error submitting the form. Please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:159 static/horizon/js/horizon.tabs.js:9 +msgid "Loading" +msgstr "" + +#: static/horizon/js/horizon.modals.js:178 +msgid "An error occurred. Please try again." +msgstr "" + +#: static/horizon/js/horizon.tables.js:47 +msgid "An error occurred while updating." +msgstr "" + +#: static/horizon/js/horizon.tables.js:145 +msgid "You have selected " +msgstr "" + +#: static/horizon/js/horizon.tables.js:158 +msgid "Confirm " +msgstr "" + +#: static/horizon/js/horizon.tables.js:159 +msgid "Please confirm your selection. This action cannot be undone." +msgstr "" + +#: static/horizon/js/horizon.tables.js:173 +msgid "Working" +msgstr "" + +#: static/horizon/js/horizon.tables.js:216 +#, c-format +msgid "Displaying %s item" +msgid_plural "Displaying %s items" +msgstr[0] "" + +#: static/horizon/js/horizon.users.js:18 +msgid "Passwords do not match." +msgstr "" diff --git a/dashboard/horizon/locale/ko_KR/LC_MESSAGES/django.mo b/dashboard/horizon/locale/ko_KR/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..8b1d891160f03db116e32a6af77218afec48fe77 GIT binary patch literal 731 zcmYk2!A}!G6vkJ@Xf~cadw7ZAqM3DDBq<9eAhbwqwP}ipCo^ov?ZS4ZnVA~v$pacJ znxLV9nATtAML1B-?7*)bEPTw znN^TtjrrPI;k2$RLWVC{*?~gDsA8aH10oRwArBzrKm!_b$9f{_KqUh%G>kq1Er9wy zQwqk_a4cCalgCV|P;d|K$I}J3i>xN3CV3TvWpb|`C{hw0*o8+WvLG4N7$%EUGY^t( zB1sZ1aVH@$rvv&=E2iWjt6HPahQR z{Bk}sI%UFf?FGR#bi0YC?Br9Z9dBAI@tx6}R;moGpY1=ZLW79ksY;jT+CJ*Fy8z1deG{cuP4~|jt`-KxYPgqeweeymD$|} Zv;U#@^8|W7-VIu9)9HfQeRFP_^$!()_7ngB literal 0 HcmV?d00001 diff --git a/dashboard/horizon/locale/ko_KR/LC_MESSAGES/django.po b/dashboard/horizon/locale/ko_KR/LC_MESSAGES/django.po new file mode 100644 index 00000000..41eb4aec --- /dev/null +++ b/dashboard/horizon/locale/ko_KR/LC_MESSAGES/django.po @@ -0,0 +1,506 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# , 2012. +msgid "" +msgstr "" +"Project-Id-Version: Horizon\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:08+0000\n" +"PO-Revision-Date: 2012-08-23 05:34+0000\n" +"Last-Translator: bluejay.kr \n" +"Language-Team: LANGUAGE \n" +"Language: ko_KR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: base.py:424 +msgid "Other" +msgstr "" + +#: decorators.py:55 +msgid "Please log in to continue." +msgstr "" + +#: decorators.py:87 +#, python-format +msgid "You are not authorized to access %s" +msgstr "%s에 접근 권한이 없습니다. " + +#: exceptions.py:283 +msgid "Unauthorized. Please try logging in again." +msgstr "인증되지 않았습니다. 다시 로깅해주시기 바랍니다. " + +#: browsers/base.py:90 +msgid "Navigation Item" +msgstr "" + +#: browsers/views.py:42 +#, python-format +msgid "Select a %s to browse." +msgstr "" + +#: conf/default.py:29 +msgid "Password is not accepted" +msgstr "" + +#: tables/actions.py:349 +msgid "Filter" +msgstr "" + +#: tables/actions.py:527 +#, python-format +msgid "%(action)s %(data_type)s" +msgstr "" + +#: tables/actions.py:561 +msgid "N/A" +msgstr "" + +#: tables/actions.py:589 +#, python-format +msgid "You do not have permission to %(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:595 +#, python-format +msgid "Unable to %(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:601 +#, python-format +msgid "%(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:611 +msgid "Delete" +msgstr "" + +#: tables/actions.py:612 +msgid "Deleted" +msgstr "" + +#: tables/base.py:275 +#, python-format +msgid "The attribute %(attr)s doesn't exist on %(obj)s." +msgstr "" + +#: tables/base.py:748 +msgid "No items to display." +msgstr "" + +#: tables/base.py:852 +msgid "Actions" +msgstr "" + +#: tables/base.py:1035 +#, python-format +msgid "No match returned for the id \"%s\"." +msgstr "" + +#: tables/base.py:1165 +msgid "Please select a row before taking that action." +msgstr "" + +#: templates/_header.html:3 +msgid "Logged in as" +msgstr "" + +#: templates/_header.html:5 +msgid "Help" +msgstr "" + +#: templates/_header.html:7 +msgid "Sign Out" +msgstr "" + +#: templates/splash.html:7 templates/auth/login.html:4 +msgid "Login" +msgstr "" + +#: templates/auth/_login.html:4 +msgid "Log In" +msgstr "" + +#: templates/auth/_login.html:14 +msgid "You don't have permissions to access:" +msgstr "" + +#: templates/auth/_login.html:16 +msgid "Login as different user or go back to" +msgstr "" + +#: templates/auth/_login.html:17 +msgid "home page" +msgstr "" + +#: templates/auth/_login.html:27 +msgid "Sign In" +msgstr "" + +#: templates/horizon/_messages.html:7 +msgid "Info: " +msgstr "" + +#: templates/horizon/_messages.html:13 +msgid "Warning: " +msgstr "" + +#: templates/horizon/_messages.html:19 +msgid "Success: " +msgstr "" + +#: templates/horizon/_messages.html:25 +msgid "Error: " +msgstr "" + +#: templates/horizon/common/_data_table.html:54 +msgid "Summary" +msgstr "" + +#: templates/horizon/common/_data_table.html:63 +#, python-format +msgid "Displaying %(counter)s item" +msgid_plural "Displaying %(counter)s items" +msgstr[0] "" + +#: templates/horizon/common/_data_table_row_actions.html:10 +msgid "More" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:4 +msgid "Quota Summary" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "Used" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "of" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +msgid "Available Instances" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:8 +msgid "Available vCPUs" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:11 +msgid "Available RAM" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:15 +msgid "Available volumes" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:18 +msgid "Available volume storage" +msgstr "" + +#: templates/horizon/common/_resource_browser.html:10 +#, python-format +msgid "Displaying %(nav_items)s item" +msgid_plural "Displaying %(nav_items)s items" +msgstr[0] "" + +#: templates/horizon/common/_resource_browser.html:11 +#, python-format +msgid "Displaying %(content_items)s item" +msgid_plural "Displaying %(content_items)s items" +msgstr[0] "" + +#: templates/horizon/common/_sidebar.html:14 +msgid "Current Project" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:5 +msgid "Select a month to query its usage" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:9 +msgid "Submit" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:14 +msgid "Active Instances" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:15 +msgid "Active RAM" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:16 +msgid "This Month's VCPU-Hours" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:17 +msgid "This Month's GB-Hours" +msgstr "" + +#: templates/horizon/common/_workflow.html:33 +msgid "Cancel" +msgstr "" + +#: templatetags/branding.py:35 +msgid "Horizon" +msgstr "" + +#: templatetags/horizon.py:109 +msgid "No Limit" +msgstr "" + +#: templatetags/horizon.py:111 templatetags/horizon.py:113 +msgid "Available" +msgstr "" + +#: templatetags/sizeformat.py:45 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "" + +#: templatetags/sizeformat.py:49 +#, python-format +msgid "%(size)d" +msgid_plural "%(size)d" +msgstr[0] "" + +#: templatetags/sizeformat.py:52 +#, python-format +msgid "%s KB" +msgstr "" + +#: templatetags/sizeformat.py:55 +#, python-format +msgid "%s MB" +msgstr "" + +#: templatetags/sizeformat.py:58 +#, python-format +msgid "%s GB" +msgstr "" + +#: templatetags/sizeformat.py:61 +#, python-format +msgid "%s TB" +msgstr "" + +#: templatetags/sizeformat.py:63 +#, python-format +msgid "%s PB" +msgstr "" + +#: test/settings.py:114 +msgid "Password must be between 8 and 18 characters." +msgstr "" + +#: test/test_dashboards/cats/dashboard.py:8 +msgid "Cute Cats" +msgstr "" + +#: test/test_dashboards/cats/dashboard.py:14 +msgid "Fierce Cats" +msgstr "" + +#: test/test_dashboards/cats/dashboard.py:19 +msgid "Cats" +msgstr "" + +#: test/test_dashboards/cats/kittens/panel.py:9 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:3 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:6 +msgid "Kittens" +msgstr "" + +#: test/test_dashboards/cats/tigers/panel.py:9 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:3 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:6 +msgid "Tigers" +msgstr "" + +#: test/test_dashboards/dogs/dashboard.py:7 +msgid "Dogs" +msgstr "" + +#: test/test_dashboards/dogs/puppies/panel.py:9 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:3 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:6 +msgid "Puppies" +msgstr "" + +#: test/tests/base.py:39 +msgid "My Dashboard" +msgstr "" + +#: test/tests/base.py:45 +msgid "My Panel" +msgstr "" + +#: test/tests/base.py:51 +msgid "Admin Panel" +msgstr "" + +#: test/tests/messages.py:32 +msgid "Giant ants are attacking San Francisco!" +msgstr "" + +#: test/tests/messages.py:46 +msgid "We are now safe from ants! Go here!" +msgstr "" + +#: test/tests/tables.py:107 +msgid "Batch" +msgstr "" + +#: test/tests/tables.py:108 +msgid "Batched" +msgstr "" + +#: test/tests/tables.py:109 test/tests/tables.py:120 +msgid "Item" +msgstr "" + +#: test/tests/tables.py:110 test/tests/tables.py:121 +msgid "Items" +msgstr "" + +#: test/tests/tables.py:118 +msgid "Down" +msgstr "" + +#: test/tests/tables.py:118 +msgid "Up" +msgstr "" + +#: test/tests/tables.py:119 +msgid "Downed" +msgstr "" + +#: test/tests/tables.py:119 +msgid "Upped" +msgstr "" + +#: test/tests/tables.py:187 +msgid "No Actions Table" +msgstr "" + +#: test/tests/tables.py:684 +msgid "Single Table" +msgstr "" + +#: test/tests/tabs.py:36 +msgid "Tab One" +msgstr "" + +#: test/tests/tabs.py:42 +msgid "Delayed Tab" +msgstr "" + +#: test/tests/tabs.py:49 +msgid "Disabled Tab" +msgstr "" + +#: test/tests/tabs.py:58 +msgid "Disallowed Tab" +msgstr "" + +#: test/tests/tabs.py:76 +msgid "Tab With My Table" +msgstr "" + +#: test/tests/tabs.py:85 +msgid "Recoverable Error Tab" +msgstr "" + +#: test/tests/workflows.py:43 +msgid "Project" +msgstr "프로젝트" + +#: test/tests/workflows.py:44 +msgid "User" +msgstr "" + +#: test/tests/workflows.py:47 +msgid "Test Action One" +msgstr "" + +#: test/tests/workflows.py:61 +msgid "Instance" +msgstr "인스턴스" + +#: test/tests/workflows.py:64 +msgid "Test Action Two" +msgstr "" + +#: test/tests/workflows.py:72 +msgid "Test Action Three" +msgstr "" + +#: test/tests/workflows.py:77 +msgid "Admin" +msgstr "" + +#: test/tests/workflows.py:80 +msgid "Admin Action" +msgstr "" + +#: utils/fields.py:46 +msgid "Incorrect format for IP address" +msgstr "" + +#: utils/fields.py:47 +msgid "Invalid version for IP address" +msgstr "" + +#: utils/fields.py:48 +msgid "Invalid subnet mask" +msgstr "" + +#: workflows/base.py:71 +msgid "Processing..." +msgstr "" + +#: workflows/base.py:467 +msgid "All available" +msgstr "" + +#: workflows/base.py:468 +msgid "Members" +msgstr "" + +#: workflows/base.py:469 +msgid "None available." +msgstr "" + +#: workflows/base.py:470 +msgid "No members." +msgstr "" + +#: workflows/base.py:569 +msgid "Save" +msgstr "" + +#: workflows/base.py:570 +#, python-format +msgid "%s completed successfully." +msgstr "" + +#: workflows/base.py:571 +#, python-format +msgid "%s did not complete." +msgstr "" diff --git a/dashboard/horizon/locale/ko_KR/LC_MESSAGES/djangojs.mo b/dashboard/horizon/locale/ko_KR/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000000000000000000000000000000000000..834b8fbe8d7dab57d6f58b1fa22a8e2e70d2e521 GIT binary patch literal 378 zcmYL^K~KUk7=|%=+R?Lz9=zd)8;J>2G*nh_vE9fRBzmh*XAQQbD@K2af6w3Ix5UVs zJn5m&*S_D+>B)BobA+5B=g2W~fwUPQW4s*V**gE3d-tZ-0kRFOv~P^Fl+4-Wfz3Es z@p6$Bc~t71^8sX2n+@FU>jo-ENi;7in0T!M4neAdrlcPyeMn-6uShba{b4u8pa`Y@ zFzw@i-Xmuv$O2ME?h7ij?o*NTNT?=2<&|x=DmYKcbSVVMndD@|CCkK^rD@3*i(6D} zy;rq{N~?ED1j}d3w(>}b@z(U5uMZAZm6_y+{gPfBO`Hwd;&)#=>>c!HxZG`(vDak% fs(hov&9a)p;NZ2jUbF{p*KT8MP08TdgsAfiRQqN_ literal 0 HcmV?d00001 diff --git a/dashboard/horizon/locale/ko_KR/LC_MESSAGES/djangojs.po b/dashboard/horizon/locale/ko_KR/LC_MESSAGES/djangojs.po new file mode 100644 index 00000000..c92986c1 --- /dev/null +++ b/dashboard/horizon/locale/ko_KR/LC_MESSAGES/djangojs.po @@ -0,0 +1,73 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:08+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: static/horizon/js/horizon.forms.js:47 +msgid "Additional information here..." +msgstr "" + +#: static/horizon/js/horizon.forms.js:53 +msgid "Filter" +msgstr "" + +#: static/horizon/js/horizon.instances.js:28 +msgid "There was a problem communicating with the server, please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:125 +msgid "There was an error submitting the form. Please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:159 static/horizon/js/horizon.tabs.js:9 +msgid "Loading" +msgstr "" + +#: static/horizon/js/horizon.modals.js:178 +msgid "An error occurred. Please try again." +msgstr "" + +#: static/horizon/js/horizon.tables.js:47 +msgid "An error occurred while updating." +msgstr "" + +#: static/horizon/js/horizon.tables.js:145 +msgid "You have selected " +msgstr "" + +#: static/horizon/js/horizon.tables.js:158 +msgid "Confirm " +msgstr "" + +#: static/horizon/js/horizon.tables.js:159 +msgid "Please confirm your selection. This action cannot be undone." +msgstr "" + +#: static/horizon/js/horizon.tables.js:173 +msgid "Working" +msgstr "" + +#: static/horizon/js/horizon.tables.js:216 +#, c-format +msgid "Displaying %s item" +msgid_plural "Displaying %s items" +msgstr[0] "" +msgstr[1] "" + +#: static/horizon/js/horizon.users.js:18 +msgid "Passwords do not match." +msgstr "" diff --git a/dashboard/horizon/locale/nl_NL/LC_MESSAGES/django.mo b/dashboard/horizon/locale/nl_NL/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..6fad2e6fe5ea87d2ccdb26c1673a21c83b90fe9e GIT binary patch literal 2887 zcmbW1ON2Gw>0t`vTky{s7(y z{^sW|gZHBUCwLFI28Z4at_SY`w}6;p+x++rKOTT|&U1eLpzoJJI;RZM{sr&}@KxVG z3EqYNo8SZB+u%0v3`qBU1L9-fqM`FIf+XLMAnp6b^Ad=U{ek9T@E?%$+=xrpgL^>Q zKkQio>HIQC`ptnGz+)iUwdMOKKz!^iG|zzVg0$}|koKJicYxo6r{JUCLE7&i9Qoxm zNOFDTx$1e=^PK0G;6qq<9()k|3B*zCGD!FQ1(JOKf*ZjN7$m!LkmmQIq4oPgnjiJ! zb6^hrV}8B?k{y;nvZL|iZ-8XC(|-Iz5LdBJL5h=ee*Pjz`u_@&UY9^R?=pBF__yae zTuSn721zaslHYfNTfk|M?mrIF{>amLo&;(CDUkGj2c+}Qc%B7m-`C(U_#KF=(@nH4 zhs^=F6@*(-n*ym1AEh5(uq~B~x}*>JlJve(2K^7|ybqP`r1B&xM;$<=`^ZL*qZUy2 zpiuejY=`L7nKU7gk9-h z){xzHqHgkIWN-2Dyc4J+$LY>aF; zz8IU1NWsfhF2c}2V9YPZ*jd)r(RKJ)77=8(NnK0FJHod6W6RQ5s6Z!OcZ8E2Rz_pl zjB-vw$JrG=A#AH23lmZ&Q*AEh71dFW&AS#vs|ss-u?e}dsi_1^*o84!HIl+g9>q2{U0?5J3l>}ci+C{>fzc@y(_EwAg>T1MV5 z(u-M5HK8M0&?0fI*dSQKJon-_vqB;@aW{!2nnLM3Td>gNWnnaiapfy2KcuxTZ3xgwaS&^HrT~`57jolpuhfudr3=NJe9i6aMzHPy-e?J9 zr5jzSO$9?&*OAq)x{PX`(L6!iP^)hq&eKYydIUQ%iJk|1V^RArKb#6NQYT z{sx~Z=Jisd@L(!ze0D3L_U zpKjT-l$u8rg$_DH8;MueXt`c|C1a3Po6;u@Sdvw;d=Yneh((Aa-V_2)l*}MaTk0EA z#>!61=DT, 2012. +# Erik-Martijn Kasimier , 2012. +msgid "" +msgstr "" +"Project-Id-Version: Horizon\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:08+0000\n" +"PO-Revision-Date: 2012-08-16 08:19+0000\n" +"Last-Translator: Erik-Martijn Kasimier \n" +"Language-Team: LANGUAGE \n" +"Language: nl_NL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: base.py:424 +msgid "Other" +msgstr "Andere" + +#: decorators.py:55 +msgid "Please log in to continue." +msgstr "Log in om door te gaan." + +#: decorators.py:87 +#, python-format +msgid "You are not authorized to access %s" +msgstr "Je bent niet geauthoriseerd om %s te bekijken" + +#: exceptions.py:283 +msgid "Unauthorized. Please try logging in again." +msgstr "Je bent niet geautoriseerd. Probeer opnieuw in te loggen." + +#: browsers/base.py:90 +msgid "Navigation Item" +msgstr "" + +#: browsers/views.py:42 +#, python-format +msgid "Select a %s to browse." +msgstr "" + +#: conf/default.py:29 +msgid "Password is not accepted" +msgstr "Wachtwoord is niet geaccepteerd" + +#: tables/actions.py:349 +msgid "Filter" +msgstr "Filter" + +#: tables/actions.py:527 +#, python-format +msgid "%(action)s %(data_type)s" +msgstr "" + +#: tables/actions.py:561 +msgid "N/A" +msgstr "" + +#: tables/actions.py:589 +#, python-format +msgid "You do not have permission to %(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:595 +#, python-format +msgid "Unable to %(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:601 +#, python-format +msgid "%(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:611 +msgid "Delete" +msgstr "Verwijder" + +#: tables/actions.py:612 +msgid "Deleted" +msgstr "Verwijderd" + +#: tables/base.py:275 +#, python-format +msgid "The attribute %(attr)s doesn't exist on %(obj)s." +msgstr "" + +#: tables/base.py:748 +msgid "No items to display." +msgstr "" + +#: tables/base.py:852 +msgid "Actions" +msgstr "Acties" + +#: tables/base.py:1035 +#, python-format +msgid "No match returned for the id \"%s\"." +msgstr "" + +#: tables/base.py:1165 +msgid "Please select a row before taking that action." +msgstr "" + +#: templates/_header.html:3 +msgid "Logged in as" +msgstr "" + +#: templates/_header.html:5 +msgid "Help" +msgstr "" + +#: templates/_header.html:7 +msgid "Sign Out" +msgstr "" + +#: templates/splash.html:7 templates/auth/login.html:4 +msgid "Login" +msgstr "" + +#: templates/auth/_login.html:4 +msgid "Log In" +msgstr "" + +#: templates/auth/_login.html:14 +msgid "You don't have permissions to access:" +msgstr "" + +#: templates/auth/_login.html:16 +msgid "Login as different user or go back to" +msgstr "" + +#: templates/auth/_login.html:17 +msgid "home page" +msgstr "" + +#: templates/auth/_login.html:27 +msgid "Sign In" +msgstr "" + +#: templates/horizon/_messages.html:7 +msgid "Info: " +msgstr "Informatie: " + +#: templates/horizon/_messages.html:13 +msgid "Warning: " +msgstr "Waarschuwing: " + +#: templates/horizon/_messages.html:19 +msgid "Success: " +msgstr "Succesvol: " + +#: templates/horizon/_messages.html:25 +msgid "Error: " +msgstr "Error: " + +#: templates/horizon/common/_data_table.html:54 +msgid "Summary" +msgstr "" + +#: templates/horizon/common/_data_table.html:63 +#, python-format +msgid "Displaying %(counter)s item" +msgid_plural "Displaying %(counter)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_data_table_row_actions.html:10 +msgid "More" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:4 +msgid "Quota Summary" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "Used" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "of" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#, fuzzy +msgid "Available Instances" +msgstr "Alle instances" + +#: templates/horizon/common/_quota_summary.html:8 +#, fuzzy +msgid "Available vCPUs" +msgstr "Beschikbaar" + +#: templates/horizon/common/_quota_summary.html:11 +#, fuzzy +msgid "Available RAM" +msgstr "Beschikbaar" + +#: templates/horizon/common/_quota_summary.html:15 +#, fuzzy +msgid "Available volumes" +msgstr "Beschikbaar" + +#: templates/horizon/common/_quota_summary.html:18 +#, fuzzy +msgid "Available volume storage" +msgstr "Alle instances" + +#: templates/horizon/common/_resource_browser.html:10 +#, python-format +msgid "Displaying %(nav_items)s item" +msgid_plural "Displaying %(nav_items)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_resource_browser.html:11 +#, python-format +msgid "Displaying %(content_items)s item" +msgid_plural "Displaying %(content_items)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_sidebar.html:14 +msgid "Current Project" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:5 +msgid "Select a month to query its usage" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:9 +msgid "Submit" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:14 +msgid "Active Instances" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:15 +msgid "Active RAM" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:16 +msgid "This Month's VCPU-Hours" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:17 +msgid "This Month's GB-Hours" +msgstr "" + +#: templates/horizon/common/_workflow.html:33 +msgid "Cancel" +msgstr "Annuleren" + +#: templatetags/branding.py:35 +msgid "Horizon" +msgstr "" + +#: templatetags/horizon.py:109 +msgid "No Limit" +msgstr "Geen limiet" + +#: templatetags/horizon.py:111 templatetags/horizon.py:113 +msgid "Available" +msgstr "Beschikbaar" + +#: templatetags/sizeformat.py:45 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "%(size)d byte" +msgstr[1] "%(size)d bytes" + +#: templatetags/sizeformat.py:49 +#, python-format +msgid "%(size)d" +msgid_plural "%(size)d" +msgstr[0] "%(size)d" +msgstr[1] "%(size)d" + +#: templatetags/sizeformat.py:52 +#, python-format +msgid "%s KB" +msgstr "%s KB" + +#: templatetags/sizeformat.py:55 +#, python-format +msgid "%s MB" +msgstr "%s MB" + +#: templatetags/sizeformat.py:58 +#, python-format +msgid "%s GB" +msgstr "%s GB" + +#: templatetags/sizeformat.py:61 +#, python-format +msgid "%s TB" +msgstr "%s TB" + +#: templatetags/sizeformat.py:63 +#, python-format +msgid "%s PB" +msgstr "%s PB" + +#: test/settings.py:114 +msgid "Password must be between 8 and 18 characters." +msgstr "" + +#: test/test_dashboards/cats/dashboard.py:8 +msgid "Cute Cats" +msgstr "Schattige katten" + +#: test/test_dashboards/cats/dashboard.py:14 +msgid "Fierce Cats" +msgstr "Felle katten" + +#: test/test_dashboards/cats/dashboard.py:19 +msgid "Cats" +msgstr "Katten" + +#: test/test_dashboards/cats/kittens/panel.py:9 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:3 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:6 +msgid "Kittens" +msgstr "Kittens" + +#: test/test_dashboards/cats/tigers/panel.py:9 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:3 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:6 +msgid "Tigers" +msgstr "Tijgers" + +#: test/test_dashboards/dogs/dashboard.py:7 +msgid "Dogs" +msgstr "Honden" + +#: test/test_dashboards/dogs/puppies/panel.py:9 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:3 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:6 +msgid "Puppies" +msgstr "Puppy's" + +#: test/tests/base.py:39 +msgid "My Dashboard" +msgstr "Mijn dashboard" + +#: test/tests/base.py:45 +msgid "My Panel" +msgstr "Alle instances" + +#: test/tests/base.py:51 +msgid "Admin Panel" +msgstr "Beheerderspaneel" + +#: test/tests/messages.py:32 +msgid "Giant ants are attacking San Francisco!" +msgstr "" + +#: test/tests/messages.py:46 +msgid "We are now safe from ants! Go here!" +msgstr "" + +#: test/tests/tables.py:107 +msgid "Batch" +msgstr "" + +#: test/tests/tables.py:108 +msgid "Batched" +msgstr "" + +#: test/tests/tables.py:109 test/tests/tables.py:120 +msgid "Item" +msgstr "Item" + +#: test/tests/tables.py:110 test/tests/tables.py:121 +msgid "Items" +msgstr "Items" + +#: test/tests/tables.py:118 +msgid "Down" +msgstr "" + +#: test/tests/tables.py:118 +msgid "Up" +msgstr "" + +#: test/tests/tables.py:119 +msgid "Downed" +msgstr "" + +#: test/tests/tables.py:119 +msgid "Upped" +msgstr "" + +#: test/tests/tables.py:187 +msgid "No Actions Table" +msgstr "" + +#: test/tests/tables.py:684 +msgid "Single Table" +msgstr "" + +#: test/tests/tabs.py:36 +msgid "Tab One" +msgstr "" + +#: test/tests/tabs.py:42 +msgid "Delayed Tab" +msgstr "" + +#: test/tests/tabs.py:49 +msgid "Disabled Tab" +msgstr "" + +#: test/tests/tabs.py:58 +msgid "Disallowed Tab" +msgstr "" + +#: test/tests/tabs.py:76 +msgid "Tab With My Table" +msgstr "" + +#: test/tests/tabs.py:85 +msgid "Recoverable Error Tab" +msgstr "" + +#: test/tests/workflows.py:43 +msgid "Project" +msgstr "Project" + +#: test/tests/workflows.py:44 +msgid "User" +msgstr "Gebruiker" + +#: test/tests/workflows.py:47 +msgid "Test Action One" +msgstr "Testactie één" + +#: test/tests/workflows.py:61 +msgid "Instance" +msgstr "Instance" + +#: test/tests/workflows.py:64 +msgid "Test Action Two" +msgstr "Testactie twee" + +#: test/tests/workflows.py:72 +msgid "Test Action Three" +msgstr "Testactie drie" + +#: test/tests/workflows.py:77 +msgid "Admin" +msgstr "Beheerder" + +#: test/tests/workflows.py:80 +msgid "Admin Action" +msgstr "Beheerdersactie" + +#: utils/fields.py:46 +msgid "Incorrect format for IP address" +msgstr "Onjuist formaat IP adres" + +#: utils/fields.py:47 +msgid "Invalid version for IP address" +msgstr "Invalide versie IP adres" + +#: utils/fields.py:48 +msgid "Invalid subnet mask" +msgstr "Invalide subnet mask" + +#: workflows/base.py:71 +msgid "Processing..." +msgstr "Verwerken..." + +#: workflows/base.py:467 +#, fuzzy +msgid "All available" +msgstr "Beschikbaar" + +#: workflows/base.py:468 +msgid "Members" +msgstr "" + +#: workflows/base.py:469 +#, fuzzy +msgid "None available." +msgstr "Beschikbaar" + +#: workflows/base.py:470 +msgid "No members." +msgstr "" + +#: workflows/base.py:569 +msgid "Save" +msgstr "Opslaan" + +#: workflows/base.py:570 +#, python-format +msgid "%s completed successfully." +msgstr "%s succesvol afgerond." + +#: workflows/base.py:571 +#, python-format +msgid "%s did not complete." +msgstr "%s was niet voltooid." diff --git a/dashboard/horizon/locale/nl_NL/LC_MESSAGES/djangojs.mo b/dashboard/horizon/locale/nl_NL/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000000000000000000000000000000000000..834b8fbe8d7dab57d6f58b1fa22a8e2e70d2e521 GIT binary patch literal 378 zcmYL^K~KUk7=|%=+R?Lz9=zd)8;J>2G*nh_vE9fRBzmh*XAQQbD@K2af6w3Ix5UVs zJn5m&*S_D+>B)BobA+5B=g2W~fwUPQW4s*V**gE3d-tZ-0kRFOv~P^Fl+4-Wfz3Es z@p6$Bc~t71^8sX2n+@FU>jo-ENi;7in0T!M4neAdrlcPyeMn-6uShba{b4u8pa`Y@ zFzw@i-Xmuv$O2ME?h7ij?o*NTNT?=2<&|x=DmYKcbSVVMndD@|CCkK^rD@3*i(6D} zy;rq{N~?ED1j}d3w(>}b@z(U5uMZAZm6_y+{gPfBO`Hwd;&)#=>>c!HxZG`(vDak% fs(hov&9a)p;NZ2jUbF{p*KT8MP08TdgsAfiRQqN_ literal 0 HcmV?d00001 diff --git a/dashboard/horizon/locale/nl_NL/LC_MESSAGES/djangojs.po b/dashboard/horizon/locale/nl_NL/LC_MESSAGES/djangojs.po new file mode 100644 index 00000000..c92986c1 --- /dev/null +++ b/dashboard/horizon/locale/nl_NL/LC_MESSAGES/djangojs.po @@ -0,0 +1,73 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:08+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: static/horizon/js/horizon.forms.js:47 +msgid "Additional information here..." +msgstr "" + +#: static/horizon/js/horizon.forms.js:53 +msgid "Filter" +msgstr "" + +#: static/horizon/js/horizon.instances.js:28 +msgid "There was a problem communicating with the server, please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:125 +msgid "There was an error submitting the form. Please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:159 static/horizon/js/horizon.tabs.js:9 +msgid "Loading" +msgstr "" + +#: static/horizon/js/horizon.modals.js:178 +msgid "An error occurred. Please try again." +msgstr "" + +#: static/horizon/js/horizon.tables.js:47 +msgid "An error occurred while updating." +msgstr "" + +#: static/horizon/js/horizon.tables.js:145 +msgid "You have selected " +msgstr "" + +#: static/horizon/js/horizon.tables.js:158 +msgid "Confirm " +msgstr "" + +#: static/horizon/js/horizon.tables.js:159 +msgid "Please confirm your selection. This action cannot be undone." +msgstr "" + +#: static/horizon/js/horizon.tables.js:173 +msgid "Working" +msgstr "" + +#: static/horizon/js/horizon.tables.js:216 +#, c-format +msgid "Displaying %s item" +msgid_plural "Displaying %s items" +msgstr[0] "" +msgstr[1] "" + +#: static/horizon/js/horizon.users.js:18 +msgid "Passwords do not match." +msgstr "" diff --git a/dashboard/horizon/locale/pl/LC_MESSAGES/django.mo b/dashboard/horizon/locale/pl/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..37009a29735b5e7f9f65e81e87120c85253260fa GIT binary patch literal 572 zcmZ`#!A|2a5DhEf&?9FKzH#Ud8&?IjURbf)f`mkAfPfG;lMJbsU`O@@0cU=JPvSG+ zTbKYPC!X|V+4J7aJio3iK1GZl%vI)3<`Q#@*&tyqGmn@r3sH2=XH(x~17F|Kc)u{l z5(%TkyZ@LG?B{TXJ{V_}a247>I=hBkhuNjmKJOf3&_Q{*lvqj7YHfp8zmA~nlKYn4s_B467<=b z*9d}GGjG&wLmN4;-}U9lll{qV`&V*3?u?uzw7m9x#rX;E=vP&0V}FZmrhP!#K27&v z(CV5cuF%7-&0KD5p~T?QQ0E*Hhq)n`r\n" +"Language-Team: Polish OpenStack translations team \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Poedit-Language: Polish\n" +"X-Poedit-Country: POLAND\n" +"X-Poedit-SourceCharset: utf-8\n" + +#: base.py:424 +msgid "Other" +msgstr "" + +#: decorators.py:55 +#, fuzzy +msgid "Please log in to continue." +msgstr "Nie można usunąć klucza: %s" + +#: decorators.py:87 +#, python-format +msgid "You are not authorized to access %s" +msgstr "" + +#: exceptions.py:283 +msgid "Unauthorized. Please try logging in again." +msgstr "" + +#: browsers/base.py:90 +msgid "Navigation Item" +msgstr "" + +#: browsers/views.py:42 +#, fuzzy, python-format +msgid "Select a %s to browse." +msgstr "Usuń projekt" + +#: conf/default.py:29 +msgid "Password is not accepted" +msgstr "" + +#: tables/actions.py:349 +msgid "Filter" +msgstr "" + +#: tables/actions.py:527 +#, python-format +msgid "%(action)s %(data_type)s" +msgstr "" + +#: tables/actions.py:561 +msgid "N/A" +msgstr "" + +#: tables/actions.py:589 +#, python-format +msgid "You do not have permission to %(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:595 +#, fuzzy, python-format +msgid "Unable to %(action)s: %(objs)s" +msgstr "Nie można usunąć klucza: %s" + +#: tables/actions.py:601 +#, python-format +msgid "%(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:611 +msgid "Delete" +msgstr "Usuń" + +#: tables/actions.py:612 +#, fuzzy +msgid "Deleted" +msgstr "Usuń" + +#: tables/base.py:275 +#, python-format +msgid "The attribute %(attr)s doesn't exist on %(obj)s." +msgstr "" + +#: tables/base.py:748 +msgid "No items to display." +msgstr "" + +#: tables/base.py:852 +#, fuzzy +msgid "Actions" +msgstr "Położenie" + +#: tables/base.py:1035 +#, python-format +msgid "No match returned for the id \"%s\"." +msgstr "" + +#: tables/base.py:1165 +msgid "Please select a row before taking that action." +msgstr "" + +#: templates/_header.html:3 +msgid "Logged in as" +msgstr "" + +#: templates/_header.html:5 +msgid "Help" +msgstr "" + +#: templates/_header.html:7 +msgid "Sign Out" +msgstr "" + +#: templates/splash.html:7 templates/auth/login.html:4 +msgid "Login" +msgstr "" + +#: templates/auth/_login.html:4 +msgid "Log In" +msgstr "" + +#: templates/auth/_login.html:14 +msgid "You don't have permissions to access:" +msgstr "" + +#: templates/auth/_login.html:16 +msgid "Login as different user or go back to" +msgstr "" + +#: templates/auth/_login.html:17 +msgid "home page" +msgstr "" + +#: templates/auth/_login.html:27 +msgid "Sign In" +msgstr "" + +#: templates/horizon/_messages.html:7 +msgid "Info: " +msgstr "" + +#: templates/horizon/_messages.html:13 +msgid "Warning: " +msgstr "" + +#: templates/horizon/_messages.html:19 +msgid "Success: " +msgstr "" + +#: templates/horizon/_messages.html:25 +msgid "Error: " +msgstr "" + +#: templates/horizon/common/_data_table.html:54 +msgid "Summary" +msgstr "" + +#: templates/horizon/common/_data_table.html:63 +#, python-format +msgid "Displaying %(counter)s item" +msgid_plural "Displaying %(counter)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_data_table_row_actions.html:10 +msgid "More" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:4 +msgid "Quota Summary" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "Used" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "of" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#, fuzzy +msgid "Available Instances" +msgstr "Instancje" + +#: templates/horizon/common/_quota_summary.html:8 +#, fuzzy +msgid "Available vCPUs" +msgstr "brak dostępnych" + +#: templates/horizon/common/_quota_summary.html:11 +#, fuzzy +msgid "Available RAM" +msgstr "brak dostępnych" + +#: templates/horizon/common/_quota_summary.html:15 +#, fuzzy +msgid "Available volumes" +msgstr "brak dostępnych" + +#: templates/horizon/common/_quota_summary.html:18 +#, fuzzy +msgid "Available volume storage" +msgstr "Instancje" + +#: templates/horizon/common/_resource_browser.html:10 +#, python-format +msgid "Displaying %(nav_items)s item" +msgid_plural "Displaying %(nav_items)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_resource_browser.html:11 +#, python-format +msgid "Displaying %(content_items)s item" +msgid_plural "Displaying %(content_items)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_sidebar.html:14 +#, fuzzy +msgid "Current Project" +msgstr "Usuń projekt" + +#: templates/horizon/common/_usage_summary.html:5 +msgid "Select a month to query its usage" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:9 +msgid "Submit" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:14 +#, fuzzy +msgid "Active Instances" +msgstr "Zobacz instancje" + +#: templates/horizon/common/_usage_summary.html:15 +msgid "Active RAM" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:16 +msgid "This Month's VCPU-Hours" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:17 +msgid "This Month's GB-Hours" +msgstr "" + +#: templates/horizon/common/_workflow.html:33 +msgid "Cancel" +msgstr "" + +#: templatetags/branding.py:35 +msgid "Horizon" +msgstr "" + +#: templatetags/horizon.py:109 +msgid "No Limit" +msgstr "" + +#: templatetags/horizon.py:111 templatetags/horizon.py:113 +#, fuzzy +msgid "Available" +msgstr "brak dostępnych" + +#: templatetags/sizeformat.py:45 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "" +msgstr[1] "" + +#: templatetags/sizeformat.py:49 +#, python-format +msgid "%(size)d" +msgid_plural "%(size)d" +msgstr[0] "" +msgstr[1] "" + +#: templatetags/sizeformat.py:52 +#, python-format +msgid "%s KB" +msgstr "" + +#: templatetags/sizeformat.py:55 +#, python-format +msgid "%s MB" +msgstr "" + +#: templatetags/sizeformat.py:58 +#, python-format +msgid "%s GB" +msgstr "" + +#: templatetags/sizeformat.py:61 +#, python-format +msgid "%s TB" +msgstr "" + +#: templatetags/sizeformat.py:63 +#, python-format +msgid "%s PB" +msgstr "" + +#: test/settings.py:114 +msgid "Password must be between 8 and 18 characters." +msgstr "" + +#: test/test_dashboards/cats/dashboard.py:8 +msgid "Cute Cats" +msgstr "" + +#: test/test_dashboards/cats/dashboard.py:14 +msgid "Fierce Cats" +msgstr "" + +#: test/test_dashboards/cats/dashboard.py:19 +msgid "Cats" +msgstr "" + +#: test/test_dashboards/cats/kittens/panel.py:9 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:3 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:6 +msgid "Kittens" +msgstr "" + +#: test/test_dashboards/cats/tigers/panel.py:9 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:3 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:6 +msgid "Tigers" +msgstr "" + +#: test/test_dashboards/dogs/dashboard.py:7 +msgid "Dogs" +msgstr "" + +#: test/test_dashboards/dogs/puppies/panel.py:9 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:3 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:6 +msgid "Puppies" +msgstr "" + +#: test/tests/base.py:39 +msgid "My Dashboard" +msgstr "" + +#: test/tests/base.py:45 +msgid "My Panel" +msgstr "" + +#: test/tests/base.py:51 +msgid "Admin Panel" +msgstr "" + +#: test/tests/messages.py:32 +msgid "Giant ants are attacking San Francisco!" +msgstr "" + +#: test/tests/messages.py:46 +msgid "We are now safe from ants! Go here!" +msgstr "" + +#: test/tests/tables.py:107 +msgid "Batch" +msgstr "" + +#: test/tests/tables.py:108 +msgid "Batched" +msgstr "" + +#: test/tests/tables.py:109 test/tests/tables.py:120 +msgid "Item" +msgstr "" + +#: test/tests/tables.py:110 test/tests/tables.py:121 +msgid "Items" +msgstr "" + +#: test/tests/tables.py:118 +msgid "Down" +msgstr "" + +#: test/tests/tables.py:118 +msgid "Up" +msgstr "" + +#: test/tests/tables.py:119 +msgid "Downed" +msgstr "" + +#: test/tests/tables.py:119 +msgid "Upped" +msgstr "" + +#: test/tests/tables.py:187 +#, fuzzy +msgid "No Actions Table" +msgstr "Położenie" + +#: test/tests/tables.py:684 +msgid "Single Table" +msgstr "" + +#: test/tests/tabs.py:36 +msgid "Tab One" +msgstr "" + +#: test/tests/tabs.py:42 +msgid "Delayed Tab" +msgstr "" + +#: test/tests/tabs.py:49 +msgid "Disabled Tab" +msgstr "" + +#: test/tests/tabs.py:58 +msgid "Disallowed Tab" +msgstr "" + +#: test/tests/tabs.py:76 +msgid "Tab With My Table" +msgstr "" + +#: test/tests/tabs.py:85 +msgid "Recoverable Error Tab" +msgstr "" + +#: test/tests/workflows.py:43 +#, fuzzy +msgid "Project" +msgstr "Usuń projekt" + +#: test/tests/workflows.py:44 +msgid "User" +msgstr "" + +#: test/tests/workflows.py:47 +msgid "Test Action One" +msgstr "" + +#: test/tests/workflows.py:61 +#, fuzzy +msgid "Instance" +msgstr "Instancje" + +#: test/tests/workflows.py:64 +msgid "Test Action Two" +msgstr "" + +#: test/tests/workflows.py:72 +msgid "Test Action Three" +msgstr "" + +#: test/tests/workflows.py:77 +msgid "Admin" +msgstr "" + +#: test/tests/workflows.py:80 +#, fuzzy +msgid "Admin Action" +msgstr "Położenie" + +#: utils/fields.py:46 +msgid "Incorrect format for IP address" +msgstr "" + +#: utils/fields.py:47 +msgid "Invalid version for IP address" +msgstr "" + +#: utils/fields.py:48 +msgid "Invalid subnet mask" +msgstr "" + +#: workflows/base.py:71 +msgid "Processing..." +msgstr "" + +#: workflows/base.py:467 +#, fuzzy +msgid "All available" +msgstr "brak dostępnych" + +#: workflows/base.py:468 +msgid "Members" +msgstr "" + +#: workflows/base.py:469 +#, fuzzy +msgid "None available." +msgstr "brak dostępnych" + +#: workflows/base.py:470 +msgid "No members." +msgstr "" + +#: workflows/base.py:569 +msgid "Save" +msgstr "" + +#: workflows/base.py:570 +#, fuzzy, python-format +msgid "%s completed successfully." +msgstr "Wolumen %(id)s %(name)s został pomyślnie utworzony." + +#: workflows/base.py:571 +#, python-format +msgid "%s did not complete." +msgstr "" diff --git a/dashboard/horizon/locale/pl/LC_MESSAGES/djangojs.mo b/dashboard/horizon/locale/pl/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000000000000000000000000000000000000..4decb38f45dbaaca88cea68ac63771e177b0734c GIT binary patch literal 478 zcmYL^!A|2a5Qag^DM!wGm;)dXJKzM6TGOzE)`f_qsnQf__vUTA*{p(HIW7W;*We9! zE}n%+LG+h?c|88c{{6PS^|C?QB!4IGkiU_CkXv=guXNa@wPn6Ew&uT@IgICcr)*Oh z&0!q%Z=-7gcOsv}*~rV)!dQoCQ&w}lT9yqKhJ!cG3hY}Y9Sz|?I>jMiettTa=%2BW18V0f}4z|@!{`mB)mj64ht)FQ%h$ohv76yU=*bSdLoVD zA+)I+nx;iqpTr(6kjZ?AypMS)>zR^x;dhTgK zxt)4+$BSB4`W$Beq-~T7r^OIEYp#v;S6LK#X6BVHIduM1&KuWDD{CAYd*5)-i`BLX zPtM_^4EJ>y`tTDN(DAO%F2Vrz_Eu#tJZ;PUw#|AzgXiZ}zh&3~&G, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:09+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2)\n" + +#: static/horizon/js/horizon.forms.js:47 +msgid "Additional information here..." +msgstr "" + +#: static/horizon/js/horizon.forms.js:53 +msgid "Filter" +msgstr "" + +#: static/horizon/js/horizon.instances.js:28 +msgid "There was a problem communicating with the server, please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:125 +msgid "There was an error submitting the form. Please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:159 static/horizon/js/horizon.tabs.js:9 +msgid "Loading" +msgstr "" + +#: static/horizon/js/horizon.modals.js:178 +msgid "An error occurred. Please try again." +msgstr "" + +#: static/horizon/js/horizon.tables.js:47 +msgid "An error occurred while updating." +msgstr "" + +#: static/horizon/js/horizon.tables.js:145 +msgid "You have selected " +msgstr "" + +#: static/horizon/js/horizon.tables.js:158 +msgid "Confirm " +msgstr "" + +#: static/horizon/js/horizon.tables.js:159 +msgid "Please confirm your selection. This action cannot be undone." +msgstr "" + +#: static/horizon/js/horizon.tables.js:173 +msgid "Working" +msgstr "" + +#: static/horizon/js/horizon.tables.js:216 +#, c-format +msgid "Displaying %s item" +msgid_plural "Displaying %s items" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: static/horizon/js/horizon.users.js:18 +msgid "Passwords do not match." +msgstr "" diff --git a/dashboard/horizon/locale/pt/LC_MESSAGES/django.mo b/dashboard/horizon/locale/pt/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..01593720facf4e0b27a46424087102f71805e03d GIT binary patch literal 4355 zcma);U2I%O6@Z5l+G1#EOWFqdHwkqcx7n?2n$&UP693z(omj~_g$Iyk_l|dOa`(=5 z=H7K&fp{nq5Aar1ARa2x2vsDl1VVs70*UMcQbFni2q7U50#$;SO7Kwi0VKY2@3oyk zDly)f@6P|3bI#21pYGoCvLY>$A0S7!C}rU5dnibww<`4^I9BoLic=M5D=x!NGVV0o z2hYRX;FsZ@@T*YP`6m1*dDH-?tn7yfrPlyev;7H@DCgLV zP{Mnm=%6eiF*LEOX*=hHpXKs95t5JW5_7%Uns~D`R9ilcXc$ zr^#~1#Q!DL$#Z1UyZA6FQBRUZuQjsxgPcv$40(clm@ImgB=U*O;@_gHXUJ1zNr<f}bSddGWO+B< zFFic@pVlx}*khfYkgpE8&w(2q^iX)x6Om7dVtLau2yEGK+TfLly%%n_G(@{50%Tkwv zs>+&4ZK9_8CQeKMSfmYy@A({#p1 zQYn>l%ITH38)u4-SjOwjq5IH73w3EkzE($jo8>;`tg=tmu^Mp>4u^wvb()%2YbJzE z=OZ15!fFO<_OR=2M>h|dZd!iXrnRY_&?b%a*o1C%j5jUx66$I#u_joZxVDZ{k)-9) zEKYN~(-klYiZj}~P1Zsjt21*UPTPouDp?72Fx)%wC)(@vqIRBXYAx^eVm#)IZ8zzQZv7U@2|5p^&4|1y3OieREQZ2W^y$VJ$%SD&yS$8 zMhBIzimZ>xZL2N0+=m-m&&{r_Z@0&76s^|N;^-WX9SV(&gvoNvFj92qS;zVK1sm0M zWt0phV3@Y38snI@iP7_Vk6#Z%uLs=VIpb6M(aGmrt_`-4D{OWa6LpGfgrJ8*F(z`w zkPfQXJ?pzMHsg5jCmCiHL)Mm~+PTv6C-ssD>e8VzwSTMg|vs?Fr>P-}3FhSp9u zYO~%N@r&A=$?T*a9~~R7jg8hukLa;uljFyp7$q^VcEVW#wcN*PUG9$g z1)EK+Hx_CWyW>R44eM+3JULNenTckc4Xq`)H%V=Q=Lfr|y`mbX#*gdrbLvp44@~K? zk>f-E&m*3;l6>$+WaP2dgDV%~%oT5VI=Jk3F=DyK)exTX!E1vbatGyk8@!sfViWXq z7WYl{kmKOqFy^u=hO$Crb@9Ys*R#f<(fCbzc%*wS9lX((m%r`vp79kUb|~L`nr0mi zLA`*UaVatfwV1~SVY~@W&0j?1*q7vVjyMpQ)HnF73@k?%XDmE7iy5Q?7nqmx*+HQX-VTJ~^V!!9AvR=DC$KrvnRb~`a8+qHP%JIoa$YXVG;Qw%PN8!qOA zTy`%)OSPr$4z5%>LcEYC85=sJ%84>~O4^TQMlNrq_)M|-dbi}@+nWD)5V(t%ZQl}! zHI_gTG{fYQVU69khG)P3ZJu*ng44Tk#*~qVM_Hy9(Z*`q-3>QIgyDXmCD~3+p{n@u zU#+vIx7EZna?@L;XI>D+-Zb14jzf@;cbv;b#M!puVXTc!wB6wU4-`}yHFf>M4E$Fx zpV&TnD!>I>5BcDdk9j$G$3<=D9E!M7o~Gvz0FT0|Uc%2g*V^vF>$juN&ds=~sQ&@e CTr2+o literal 0 HcmV?d00001 diff --git a/dashboard/horizon/locale/pt/LC_MESSAGES/django.po b/dashboard/horizon/locale/pt/LC_MESSAGES/django.po new file mode 100644 index 00000000..a134f290 --- /dev/null +++ b/dashboard/horizon/locale/pt/LC_MESSAGES/django.po @@ -0,0 +1,521 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# FIRST AUTHOR Jeffrey Wilcox, 2011. +msgid "" +msgstr "" +"Project-Id-Version: Horizon\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:08+0000\n" +"PO-Revision-Date: 2012-08-16 08:19+0000\n" +"Last-Translator: Gabriel Hurley \n" +"Language-Team: LANGUAGE \n" +"Language: pt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: base.py:424 +msgid "Other" +msgstr "Outro" + +#: decorators.py:55 +msgid "Please log in to continue." +msgstr "Por favor login para continuar" + +#: decorators.py:87 +#, python-format +msgid "You are not authorized to access %s" +msgstr "Voçê não pode acessar %s" + +#: exceptions.py:283 +msgid "Unauthorized. Please try logging in again." +msgstr "Não autorizado. Por favor, tente efetuar login novamente." + +#: browsers/base.py:90 +msgid "Navigation Item" +msgstr "" + +#: browsers/views.py:42 +#, python-format +msgid "Select a %s to browse." +msgstr "" + +#: conf/default.py:29 +msgid "Password is not accepted" +msgstr "Senha não é aceita" + +#: tables/actions.py:349 +msgid "Filter" +msgstr "Filtro" + +#: tables/actions.py:527 +#, python-format +msgid "%(action)s %(data_type)s" +msgstr "" + +#: tables/actions.py:561 +msgid "N/A" +msgstr "" + +#: tables/actions.py:589 +#, python-format +msgid "You do not have permission to %(action)s: %(objs)s" +msgstr "Você não tem permissão para %(action)s: %(objs)s" + +#: tables/actions.py:595 +#, python-format +msgid "Unable to %(action)s: %(objs)s" +msgstr "Incapaz de %(action)s: %(objs)s" + +#: tables/actions.py:601 +#, python-format +msgid "%(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:611 +msgid "Delete" +msgstr "Excluir" + +#: tables/actions.py:612 +msgid "Deleted" +msgstr "Excluído" + +#: tables/base.py:275 +#, python-format +msgid "The attribute %(attr)s doesn't exist on %(obj)s." +msgstr "O %(attr)s atributo não existe em %(obj)s." + +#: tables/base.py:748 +msgid "No items to display." +msgstr "Não há itens para mostrar." + +#: tables/base.py:852 +msgid "Actions" +msgstr "Ações" + +#: tables/base.py:1035 +#, python-format +msgid "No match returned for the id \"%s\"." +msgstr "No jogo voltou para a ID de \"%s\"." + +#: tables/base.py:1165 +msgid "Please select a row before taking that action." +msgstr "Por favor, selecione uma linha antes de tomar essa ação." + +#: templates/_header.html:3 +msgid "Logged in as" +msgstr "" + +#: templates/_header.html:5 +msgid "Help" +msgstr "" + +#: templates/_header.html:7 +msgid "Sign Out" +msgstr "" + +#: templates/splash.html:7 templates/auth/login.html:4 +msgid "Login" +msgstr "" + +#: templates/auth/_login.html:4 +msgid "Log In" +msgstr "" + +#: templates/auth/_login.html:14 +#, fuzzy +msgid "You don't have permissions to access:" +msgstr "Você não tem permissão para %(action)s: %(objs)s" + +#: templates/auth/_login.html:16 +msgid "Login as different user or go back to" +msgstr "" + +#: templates/auth/_login.html:17 +msgid "home page" +msgstr "" + +#: templates/auth/_login.html:27 +msgid "Sign In" +msgstr "" + +#: templates/horizon/_messages.html:7 +msgid "Info: " +msgstr "Informações: " + +#: templates/horizon/_messages.html:13 +msgid "Warning: " +msgstr "Aviso: " + +#: templates/horizon/_messages.html:19 +msgid "Success: " +msgstr "Sucesso: " + +#: templates/horizon/_messages.html:25 +msgid "Error: " +msgstr "Erro: " + +#: templates/horizon/common/_data_table.html:54 +msgid "Summary" +msgstr "" + +#: templates/horizon/common/_data_table.html:63 +#, python-format +msgid "Displaying %(counter)s item" +msgid_plural "Displaying %(counter)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_data_table_row_actions.html:10 +msgid "More" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:4 +#, fuzzy +msgid "Quota Summary" +msgstr "Resumo de Utilização" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "Used" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "of" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#, fuzzy +msgid "Available Instances" +msgstr "Todas as Instâncias" + +#: templates/horizon/common/_quota_summary.html:8 +#, fuzzy +msgid "Available vCPUs" +msgstr "Disponível" + +#: templates/horizon/common/_quota_summary.html:11 +#, fuzzy +msgid "Available RAM" +msgstr "Disponível" + +#: templates/horizon/common/_quota_summary.html:15 +#, fuzzy +msgid "Available volumes" +msgstr "Disponível" + +#: templates/horizon/common/_quota_summary.html:18 +#, fuzzy +msgid "Available volume storage" +msgstr "Todas as Instâncias" + +#: templates/horizon/common/_resource_browser.html:10 +#, python-format +msgid "Displaying %(nav_items)s item" +msgid_plural "Displaying %(nav_items)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_resource_browser.html:11 +#, python-format +msgid "Displaying %(content_items)s item" +msgid_plural "Displaying %(content_items)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_sidebar.html:14 +msgid "Current Project" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:5 +msgid "Select a month to query its usage" +msgstr "Selecione um mês para consultar o seu uso" + +#: templates/horizon/common/_usage_summary.html:9 +msgid "Submit" +msgstr "Enviar" + +#: templates/horizon/common/_usage_summary.html:14 +msgid "Active Instances" +msgstr "Instâncias Ativas" + +#: templates/horizon/common/_usage_summary.html:15 +#, fuzzy +msgid "Active RAM" +msgstr "Memória Ativa" + +#: templates/horizon/common/_usage_summary.html:16 +msgid "This Month's VCPU-Hours" +msgstr "VCPU-Horas deste mês" + +#: templates/horizon/common/_usage_summary.html:17 +msgid "This Month's GB-Hours" +msgstr "GB-Horas deste mês" + +#: templates/horizon/common/_workflow.html:33 +msgid "Cancel" +msgstr "Cancelar" + +#: templatetags/branding.py:35 +msgid "Horizon" +msgstr "" + +#: templatetags/horizon.py:109 +msgid "No Limit" +msgstr "Nenhum Limite" + +#: templatetags/horizon.py:111 templatetags/horizon.py:113 +msgid "Available" +msgstr "Disponível" + +#: templatetags/sizeformat.py:45 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "" +msgstr[1] "" + +#: templatetags/sizeformat.py:49 +#, python-format +msgid "%(size)d" +msgid_plural "%(size)d" +msgstr[0] "" +msgstr[1] "" + +#: templatetags/sizeformat.py:52 +#, python-format +msgid "%s KB" +msgstr "%s KB" + +#: templatetags/sizeformat.py:55 +#, python-format +msgid "%s MB" +msgstr "%s MB" + +#: templatetags/sizeformat.py:58 +#, python-format +msgid "%s GB" +msgstr "%s GB" + +#: templatetags/sizeformat.py:61 +#, python-format +msgid "%s TB" +msgstr "%s TB" + +#: templatetags/sizeformat.py:63 +#, python-format +msgid "%s PB" +msgstr "%s PB" + +#: test/settings.py:114 +msgid "Password must be between 8 and 18 characters." +msgstr "A senha deve ter entre 8 e 18 caracteres." + +#: test/test_dashboards/cats/dashboard.py:8 +msgid "Cute Cats" +msgstr "Coatis Bonitos" + +#: test/test_dashboards/cats/dashboard.py:14 +msgid "Fierce Cats" +msgstr "Maluco Coatis" + +#: test/test_dashboards/cats/dashboard.py:19 +msgid "Cats" +msgstr "Coatis" + +#: test/test_dashboards/cats/kittens/panel.py:9 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:3 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:6 +msgid "Kittens" +msgstr "Coatinhos" + +#: test/test_dashboards/cats/tigers/panel.py:9 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:3 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:6 +msgid "Tigers" +msgstr "Capybaras" + +#: test/test_dashboards/dogs/dashboard.py:7 +msgid "Dogs" +msgstr "Macacas" + +#: test/test_dashboards/dogs/puppies/panel.py:9 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:3 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:6 +msgid "Puppies" +msgstr "Macacas" + +#: test/tests/base.py:39 +msgid "My Dashboard" +msgstr "Meu Dashboard" + +#: test/tests/base.py:45 +msgid "My Panel" +msgstr "Meu Painel" + +#: test/tests/base.py:51 +msgid "Admin Panel" +msgstr "Painel de Administração" + +#: test/tests/messages.py:32 +msgid "Giant ants are attacking San Francisco!" +msgstr "" + +#: test/tests/messages.py:46 +msgid "We are now safe from ants! Go here!" +msgstr "" + +#: test/tests/tables.py:107 +msgid "Batch" +msgstr "Agrupar" + +#: test/tests/tables.py:108 +msgid "Batched" +msgstr "Agrupadas" + +#: test/tests/tables.py:109 test/tests/tables.py:120 +msgid "Item" +msgstr "Item" + +#: test/tests/tables.py:110 test/tests/tables.py:121 +msgid "Items" +msgstr "Itens" + +#: test/tests/tables.py:118 +msgid "Down" +msgstr "Desativar" + +#: test/tests/tables.py:118 +msgid "Up" +msgstr "Elevar" + +#: test/tests/tables.py:119 +msgid "Downed" +msgstr "Desativado" + +#: test/tests/tables.py:119 +msgid "Upped" +msgstr "Elevado" + +#: test/tests/tables.py:187 +msgid "No Actions Table" +msgstr "" + +#: test/tests/tables.py:684 +msgid "Single Table" +msgstr "" + +#: test/tests/tabs.py:36 +msgid "Tab One" +msgstr "Guia Um" + +#: test/tests/tabs.py:42 +msgid "Delayed Tab" +msgstr "Guia Atraso" + +#: test/tests/tabs.py:49 +msgid "Disabled Tab" +msgstr "Desativado Guia" + +#: test/tests/tabs.py:58 +msgid "Disallowed Tab" +msgstr "Guia não permitido" + +#: test/tests/tabs.py:76 +msgid "Tab With My Table" +msgstr "Guia à minha mesa" + +#: test/tests/tabs.py:85 +msgid "Recoverable Error Tab" +msgstr "Guia de Erro Recuperável" + +#: test/tests/workflows.py:43 +msgid "Project" +msgstr "Projeto" + +#: test/tests/workflows.py:44 +msgid "User" +msgstr "Usuário" + +#: test/tests/workflows.py:47 +msgid "Test Action One" +msgstr "Ação Teste dos Um" + +#: test/tests/workflows.py:61 +msgid "Instance" +msgstr "Instância" + +#: test/tests/workflows.py:64 +msgid "Test Action Two" +msgstr "Ação Teste dos Dois" + +#: test/tests/workflows.py:72 +msgid "Test Action Three" +msgstr "Ação Teste dos Três" + +#: test/tests/workflows.py:77 +msgid "Admin" +msgstr "Administração" + +#: test/tests/workflows.py:80 +msgid "Admin Action" +msgstr "Ação de Administração" + +#: utils/fields.py:46 +msgid "Incorrect format for IP address" +msgstr "Formato incorreto para o endereço IP" + +#: utils/fields.py:47 +msgid "Invalid version for IP address" +msgstr "Inválido versão para o endereço IP" + +#: utils/fields.py:48 +msgid "Invalid subnet mask" +msgstr "Máscara de sub-rede inválida" + +#: workflows/base.py:71 +msgid "Processing..." +msgstr "Processamento..." + +#: workflows/base.py:467 +#, fuzzy +msgid "All available" +msgstr "Disponível" + +#: workflows/base.py:468 +msgid "Members" +msgstr "" + +#: workflows/base.py:469 +#, fuzzy +msgid "None available." +msgstr "Disponível" + +#: workflows/base.py:470 +msgid "No members." +msgstr "" + +#: workflows/base.py:569 +msgid "Save" +msgstr "Guardar" + +#: workflows/base.py:570 +#, python-format +msgid "%s completed successfully." +msgstr "%s concluída com êxito." + +#: workflows/base.py:571 +#, python-format +msgid "%s did not complete." +msgstr "%s não foi concluída." diff --git a/dashboard/horizon/locale/pt/LC_MESSAGES/djangojs.mo b/dashboard/horizon/locale/pt/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000000000000000000000000000000000000..d7f6e46db307e57cd0670398bbe4a171c800aef4 GIT binary patch literal 420 zcmYL^PfNov9EKG=_2}6{1P{vm&2}Jc*(!@`HjDj7*A4U*w^%#6q$HW>2l4CqSxi;j z3lB-iljP0U$?@lb&7tMga&9@YTv^)mEgQQzuw(1|CsT66R|dj0EV@<3)3 zEonXv)5OVnBb9+zm(?2XcBO_wV&r6L0TacUv4Z#&rKbuj@J!DGV3(xsS&7HvHXOH#Muo z5UpOB(%krqVha7e*VcOB3aD7gbtOuS`p=bdvU;aj4O6MM8Y9vCEVbVoqTi{1A<&tR Iyi4cc8?IM$3jhEB literal 0 HcmV?d00001 diff --git a/dashboard/horizon/locale/pt/LC_MESSAGES/djangojs.po b/dashboard/horizon/locale/pt/LC_MESSAGES/djangojs.po new file mode 100644 index 00000000..03ebd97c --- /dev/null +++ b/dashboard/horizon/locale/pt/LC_MESSAGES/djangojs.po @@ -0,0 +1,74 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:08+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +#: static/horizon/js/horizon.forms.js:47 +msgid "Additional information here..." +msgstr "" + +#: static/horizon/js/horizon.forms.js:53 +msgid "Filter" +msgstr "" + +#: static/horizon/js/horizon.instances.js:28 +msgid "There was a problem communicating with the server, please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:125 +msgid "There was an error submitting the form. Please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:159 static/horizon/js/horizon.tabs.js:9 +msgid "Loading" +msgstr "" + +#: static/horizon/js/horizon.modals.js:178 +msgid "An error occurred. Please try again." +msgstr "" + +#: static/horizon/js/horizon.tables.js:47 +msgid "An error occurred while updating." +msgstr "" + +#: static/horizon/js/horizon.tables.js:145 +msgid "You have selected " +msgstr "" + +#: static/horizon/js/horizon.tables.js:158 +msgid "Confirm " +msgstr "" + +#: static/horizon/js/horizon.tables.js:159 +msgid "Please confirm your selection. This action cannot be undone." +msgstr "" + +#: static/horizon/js/horizon.tables.js:173 +msgid "Working" +msgstr "" + +#: static/horizon/js/horizon.tables.js:216 +#, c-format +msgid "Displaying %s item" +msgid_plural "Displaying %s items" +msgstr[0] "" +msgstr[1] "" + +#: static/horizon/js/horizon.users.js:18 +msgid "Passwords do not match." +msgstr "" diff --git a/dashboard/horizon/locale/pt_BR/LC_MESSAGES/django.mo b/dashboard/horizon/locale/pt_BR/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..03aa28694bbbc037c93ad377ef2bd94b786e78f2 GIT binary patch literal 4507 zcmb`JU2I%O6@aIqKw}Dl@|RMelTwqo&2DX%faClS$3KZ1J6UWeEvogakzhK_5UBLP$t`;eqeWy^a$S z`o#6le0P5CnVECWoZ~;l==|-0{j46g);8~d?$Rm z;&&=us(7X1HTW^+y#PhdYw+FhFYx{FO(=5y4ey3q7<>=h2DifdAVX>x%9eJ+ZE&m_ zpQ^@>L4In9Y65;4%KUFv{m(#=`z#cBKZZNu3sC004*97+QQZ&!3T571cTwOrst>~L z@RM*il<$r~*?$Ad_otwI_f;t8vDLV*+7}={^#qli<6Bkx`%v_A1&X|v;db~ctijiz z=y5Abslx~1d*L#abxp+t%D&FQ9k5^ZKLZh=o`tf%tB{|1o=U!d3Cez6g<|JlSM4|8 zhiU%{eiRN3mES)AMGqtJA$SzZx&bm(U4Wm0-+<%rhwzCzl=>Z%bDl;yvQ7@gpPqzr z?#mUgR{Uwj7c2e@iXL8p_rO0u+5bPP_FGW&GDIc&-412{qfqQW2SpD{Q1tK>$WJvZ z_Mpi7I+S@&Lh+}ks{UU<+2^kyKlNKGk@E(u!8f6d-}}B&&izpAuoE(*<{>Ijk3%^} z6Ar_#LDAo3DBu4G%6@+WMgB{Wqp4S_@z|pf3MniU`$Q>ZYc6mw-SGAvqBJbyLC;TOxgl|ESH;$9Ye)d7}r)l^o>_FTpo(-rGihNh6JVp_j zavrgr%>O)vqr^x2_F;X*pP-1YM9@h44rfFjRP%2<5ctT2PxQ#E(MLlp5Jxm#inv5mwjv2lEc zcbQh6xNIa$>fv3kc_xemHM}b%7wt&<))(De&+YZ@*kXFnM-EGUEY^#$Zp3;e)-BiV zr8c*19g0@VhOk3Us0X(w%+xm<5dsjI2DpAgx%dOi!e$@oZ3wYy1H*1DWjH5w+f zshaAWBsI;{s%e>@f;_0{!h4(Ly5Zd!+sbL@R>zSUD_c?}+G-{Vz0|BHSqD9|T#@D0 zj|83Mw)=KtLCv^MP_y1UKdIGRVtuPxU@l3Kp^hYm^(jFcZ?(yD(>g25ESpTvdG?xw zmfN9@+O(&RI-gv0nVQd9j*Yi+z3P0|MCkd3Htn`Ye~hm>mfKJcFMX4u%1|^jo9nI# zXRr75tq)jOB;6!JPBu>!)UhPbZ5Gsm>*)DR(7{$orcLZa>x=7p#)P$|Grld2Qsu`@ zKj|28A1#8^ai#%<^aRRl>#+&lS~DJR%3N=L+lBIb+a8Fdb$j9dwYD zJW=FoxoC!fRLe!TYy7%ev7D}SUwtC8H?>#Ry#249^G>a-p{hk$d8b^o+EG{d zaC7ge>Bh;8{-lF~)X7Y)f^7A+H=N-jUlV7u?Ygd%nR6r*+7agz<9AGw)z!&>)gCuK zqnmSl$raI$<mjiIv*4x6%J%3rHhQm9(d36#zp!v9P3q2fHs?(0 zUVi%U($M1k;_T+p$Lga))0mvlRa;r_akSiC$R8m>CE31s5rWMRoLrf!O>EASov&J7 znuh92noH%hvPeyyZ_W#oR-J^8)Lt}d$#xyl zv}DqDYSGCCGEQ=9(s-M(pcYMwzcO!3ZmiNZY0+}zkTmxIdl6lO?oi_iK@&MdKL%fm zYc5NxVTzROF?FVBo9pRgHfvjN2UnZ~{LEEJgrHoemE56G0%F&o-^*_D2K}nV!KKh5 z&WbL*rAo5?;1apTD7l!j4ds5i3!wn$6IZm)wHE#PBRD z4X+k$abt3!Raz}dl5LP?Y^U5Rp}4D4$uU+Pm%Z2v_yB!nx{3SgMx%HVP9b^>Hd`|~ zde*8-h*CDmjhYHN4#*Ga1w(lv%S037vYmKYJOUCv&-w{cNKbP2CK(Pz zmsBG=5_g`8U+I(GTe?;}%iYpD$Gy?W*O%`pPt!>zl6hOZqAXmwe*XRBC;0Ce+K179rw literal 0 HcmV?d00001 diff --git a/dashboard/horizon/locale/pt_BR/LC_MESSAGES/django.po b/dashboard/horizon/locale/pt_BR/LC_MESSAGES/django.po new file mode 100644 index 00000000..6f4a4042 --- /dev/null +++ b/dashboard/horizon/locale/pt_BR/LC_MESSAGES/django.po @@ -0,0 +1,525 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Leonardo Rodrigues de Mello <>, 2012. +# Leonardo Rodrigues de Mello , 2012. +# Marcelo Dieder , 2012. +# , 2012. +# , 2012. +msgid "" +msgstr "" +"Project-Id-Version: Horizon\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:08+0000\n" +"PO-Revision-Date: 2012-08-29 15:12+0000\n" +"Last-Translator: Leonardo Rodrigues de Mello \n" +"Language-Team: LANGUAGE \n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: base.py:424 +msgid "Other" +msgstr "Outro" + +#: decorators.py:55 +msgid "Please log in to continue." +msgstr "Por favor faça login para continuar." + +#: decorators.py:87 +#, python-format +msgid "You are not authorized to access %s" +msgstr "Você está autorizado a acessar %s" + +#: exceptions.py:283 +msgid "Unauthorized. Please try logging in again." +msgstr "Não autorizado. Por favor, tente efetuar login novamente." + +#: browsers/base.py:90 +msgid "Navigation Item" +msgstr "Item de Navegação" + +#: browsers/views.py:42 +#, python-format +msgid "Select a %s to browse." +msgstr "" + +#: conf/default.py:29 +msgid "Password is not accepted" +msgstr "senha não foi aceita" + +#: tables/actions.py:349 +msgid "Filter" +msgstr "Filtro" + +#: tables/actions.py:527 +#, python-format +msgid "%(action)s %(data_type)s" +msgstr "" + +#: tables/actions.py:561 +msgid "N/A" +msgstr "" + +#: tables/actions.py:589 +#, python-format +msgid "You do not have permission to %(action)s: %(objs)s" +msgstr "Você não tem permissão para %(action)s: %(objs)s" + +#: tables/actions.py:595 +#, python-format +msgid "Unable to %(action)s: %(objs)s" +msgstr "Não foi possível %(action)s: %(objs)s" + +#: tables/actions.py:601 +#, python-format +msgid "%(action)s: %(objs)s" +msgstr "%(action)s: %(objs)s" + +#: tables/actions.py:611 +msgid "Delete" +msgstr "Remover" + +#: tables/actions.py:612 +msgid "Deleted" +msgstr "Removido" + +#: tables/base.py:275 +#, python-format +msgid "The attribute %(attr)s doesn't exist on %(obj)s." +msgstr "" + +#: tables/base.py:748 +msgid "No items to display." +msgstr "Não existem items para mostrar." + +#: tables/base.py:852 +msgid "Actions" +msgstr "Ações" + +#: tables/base.py:1035 +#, python-format +msgid "No match returned for the id \"%s\"." +msgstr "" + +#: tables/base.py:1165 +msgid "Please select a row before taking that action." +msgstr "Por favor selecione uma célula antes de realizar esta ação" + +#: templates/_header.html:3 +msgid "Logged in as" +msgstr "Logado como:" + +#: templates/_header.html:5 +msgid "Help" +msgstr "Ajuda" + +#: templates/_header.html:7 +msgid "Sign Out" +msgstr "Sair" + +#: templates/splash.html:7 templates/auth/login.html:4 +msgid "Login" +msgstr "Logar" + +#: templates/auth/_login.html:4 +msgid "Log In" +msgstr "Entrar" + +#: templates/auth/_login.html:14 +#, fuzzy +msgid "You don't have permissions to access:" +msgstr "Você não tem permissão para %(action)s: %(objs)s" + +#: templates/auth/_login.html:16 +msgid "Login as different user or go back to" +msgstr "" + +#: templates/auth/_login.html:17 +msgid "home page" +msgstr "" + +#: templates/auth/_login.html:27 +msgid "Sign In" +msgstr "Registrar" + +#: templates/horizon/_messages.html:7 +msgid "Info: " +msgstr "Informação:" + +#: templates/horizon/_messages.html:13 +msgid "Warning: " +msgstr "Alerta:" + +#: templates/horizon/_messages.html:19 +msgid "Success: " +msgstr "Sucesso:" + +#: templates/horizon/_messages.html:25 +msgid "Error: " +msgstr "Erro:" + +#: templates/horizon/common/_data_table.html:54 +msgid "Summary" +msgstr "Resumo" + +#: templates/horizon/common/_data_table.html:63 +#, python-format +msgid "Displaying %(counter)s item" +msgid_plural "Displaying %(counter)s items" +msgstr[0] "Mostrando %(counter)s item" +msgstr[1] "Mostrando %(counter)s items" + +#: templates/horizon/common/_data_table_row_actions.html:10 +msgid "More" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:4 +#, fuzzy +msgid "Quota Summary" +msgstr "Resumo" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "Used" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "of" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#, fuzzy +msgid "Available Instances" +msgstr "Todas as Instâncias" + +#: templates/horizon/common/_quota_summary.html:8 +#, fuzzy +msgid "Available vCPUs" +msgstr "Disponível" + +#: templates/horizon/common/_quota_summary.html:11 +#, fuzzy +msgid "Available RAM" +msgstr "Disponível" + +#: templates/horizon/common/_quota_summary.html:15 +#, fuzzy +msgid "Available volumes" +msgstr "Disponível" + +#: templates/horizon/common/_quota_summary.html:18 +#, fuzzy +msgid "Available volume storage" +msgstr "Todas as Instâncias" + +#: templates/horizon/common/_resource_browser.html:10 +#, python-format +msgid "Displaying %(nav_items)s item" +msgid_plural "Displaying %(nav_items)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_resource_browser.html:11 +#, python-format +msgid "Displaying %(content_items)s item" +msgid_plural "Displaying %(content_items)s items" +msgstr[0] "" +msgstr[1] "" + +#: templates/horizon/common/_sidebar.html:14 +msgid "Current Project" +msgstr "projeto atual" + +#: templates/horizon/common/_usage_summary.html:5 +msgid "Select a month to query its usage" +msgstr "Escolha um mês para consultar sua utilização" + +#: templates/horizon/common/_usage_summary.html:9 +msgid "Submit" +msgstr "Enviar" + +#: templates/horizon/common/_usage_summary.html:14 +msgid "Active Instances" +msgstr "Instâncias ativas" + +#: templates/horizon/common/_usage_summary.html:15 +#, fuzzy +msgid "Active RAM" +msgstr "Memória Ativa" + +#: templates/horizon/common/_usage_summary.html:16 +msgid "This Month's VCPU-Hours" +msgstr "VCPU-Horas este mês" + +#: templates/horizon/common/_usage_summary.html:17 +msgid "This Month's GB-Hours" +msgstr "GB-Horas este mês" + +#: templates/horizon/common/_workflow.html:33 +msgid "Cancel" +msgstr "Cancelar" + +#: templatetags/branding.py:35 +msgid "Horizon" +msgstr "Horizon" + +#: templatetags/horizon.py:109 +msgid "No Limit" +msgstr "Sem limite" + +#: templatetags/horizon.py:111 templatetags/horizon.py:113 +msgid "Available" +msgstr "Disponível" + +#: templatetags/sizeformat.py:45 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "%(size)d byte" +msgstr[1] "%(size)d bytes" + +#: templatetags/sizeformat.py:49 +#, python-format +msgid "%(size)d" +msgid_plural "%(size)d" +msgstr[0] "%(size)d" +msgstr[1] "%(size)d" + +#: templatetags/sizeformat.py:52 +#, python-format +msgid "%s KB" +msgstr "%s KB" + +#: templatetags/sizeformat.py:55 +#, python-format +msgid "%s MB" +msgstr "%s MB" + +#: templatetags/sizeformat.py:58 +#, python-format +msgid "%s GB" +msgstr "%s GB" + +#: templatetags/sizeformat.py:61 +#, python-format +msgid "%s TB" +msgstr "%s TB" + +#: templatetags/sizeformat.py:63 +#, python-format +msgid "%s PB" +msgstr "%s TB" + +#: test/settings.py:114 +msgid "Password must be between 8 and 18 characters." +msgstr "As senhas devem ter entre 8 e 18 caracteres." + +#: test/test_dashboards/cats/dashboard.py:8 +msgid "Cute Cats" +msgstr "quatis beleza" + +#: test/test_dashboards/cats/dashboard.py:14 +msgid "Fierce Cats" +msgstr "quatis maluco" + +#: test/test_dashboards/cats/dashboard.py:19 +msgid "Cats" +msgstr "quatis" + +#: test/test_dashboards/cats/kittens/panel.py:9 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:3 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:6 +msgid "Kittens" +msgstr "bebê quati" + +#: test/test_dashboards/cats/tigers/panel.py:9 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:3 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:6 +msgid "Tigers" +msgstr "Capybaras" + +#: test/test_dashboards/dogs/dashboard.py:7 +msgid "Dogs" +msgstr "Macacas" + +#: test/test_dashboards/dogs/puppies/panel.py:9 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:3 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:6 +msgid "Puppies" +msgstr "Macacos pequenos" + +#: test/tests/base.py:39 +msgid "My Dashboard" +msgstr "Meu Dashboard" + +#: test/tests/base.py:45 +msgid "My Panel" +msgstr "Meu Painel" + +#: test/tests/base.py:51 +msgid "Admin Panel" +msgstr "Painel Administrativo" + +#: test/tests/messages.py:32 +msgid "Giant ants are attacking San Francisco!" +msgstr "Formigas gigantes estão atacando São Paulo!" + +#: test/tests/messages.py:46 +msgid "We are now safe from ants! Go here!" +msgstr "" + +#: test/tests/tables.py:107 +msgid "Batch" +msgstr "" + +#: test/tests/tables.py:108 +msgid "Batched" +msgstr "" + +#: test/tests/tables.py:109 test/tests/tables.py:120 +msgid "Item" +msgstr "Item" + +#: test/tests/tables.py:110 test/tests/tables.py:121 +msgid "Items" +msgstr "Itens" + +#: test/tests/tables.py:118 +msgid "Down" +msgstr "" + +#: test/tests/tables.py:118 +msgid "Up" +msgstr "" + +#: test/tests/tables.py:119 +msgid "Downed" +msgstr "" + +#: test/tests/tables.py:119 +msgid "Upped" +msgstr "" + +#: test/tests/tables.py:187 +msgid "No Actions Table" +msgstr "" + +#: test/tests/tables.py:684 +msgid "Single Table" +msgstr "" + +#: test/tests/tabs.py:36 +msgid "Tab One" +msgstr "" + +#: test/tests/tabs.py:42 +msgid "Delayed Tab" +msgstr "" + +#: test/tests/tabs.py:49 +msgid "Disabled Tab" +msgstr "" + +#: test/tests/tabs.py:58 +msgid "Disallowed Tab" +msgstr "" + +#: test/tests/tabs.py:76 +msgid "Tab With My Table" +msgstr "" + +#: test/tests/tabs.py:85 +msgid "Recoverable Error Tab" +msgstr "" + +#: test/tests/workflows.py:43 +msgid "Project" +msgstr "Projeto" + +#: test/tests/workflows.py:44 +msgid "User" +msgstr "Usuário" + +#: test/tests/workflows.py:47 +msgid "Test Action One" +msgstr "Testando Ação Um" + +#: test/tests/workflows.py:61 +msgid "Instance" +msgstr "Instância" + +#: test/tests/workflows.py:64 +msgid "Test Action Two" +msgstr "Testando Ação Dois" + +#: test/tests/workflows.py:72 +msgid "Test Action Three" +msgstr "Testando Ação Três" + +#: test/tests/workflows.py:77 +msgid "Admin" +msgstr "Administrador" + +#: test/tests/workflows.py:80 +msgid "Admin Action" +msgstr "Ação de Administrador" + +#: utils/fields.py:46 +msgid "Incorrect format for IP address" +msgstr "Endereço IP no formato incorreto" + +#: utils/fields.py:47 +msgid "Invalid version for IP address" +msgstr "Versão inválida para o endereço IP" + +#: utils/fields.py:48 +msgid "Invalid subnet mask" +msgstr "Máscara de rede inválida" + +#: workflows/base.py:71 +msgid "Processing..." +msgstr "Processando..." + +#: workflows/base.py:467 +#, fuzzy +msgid "All available" +msgstr "Disponível" + +#: workflows/base.py:468 +msgid "Members" +msgstr "" + +#: workflows/base.py:469 +#, fuzzy +msgid "None available." +msgstr "Disponível" + +#: workflows/base.py:470 +msgid "No members." +msgstr "" + +#: workflows/base.py:569 +msgid "Save" +msgstr "Salvar" + +#: workflows/base.py:570 +#, python-format +msgid "%s completed successfully." +msgstr "%s concluído com sucesso." + +#: workflows/base.py:571 +#, python-format +msgid "%s did not complete." +msgstr "%s não completou." diff --git a/dashboard/horizon/locale/pt_BR/LC_MESSAGES/djangojs.mo b/dashboard/horizon/locale/pt_BR/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000000000000000000000000000000000000..d25b405243b512eeae50070e456f6671a39abab4 GIT binary patch literal 419 zcmYL^O;5uh9EWH0)T3t)96V^|f3{6bHujR?%4TEV(sfJph8xzJ4jN!aKZsw?&mvXh ze#sLcJiw2y)05AR&5`BIa&9@cTv^)mEtTCI*s*o~lPUSiHwMBjthv&a6c}Y>c25=* zt!cgt)5OVnBb9+zm(><-_oaqHV&r6L0W-y!v4UZVKktE(9O4uLg6>irhuz0Cp-#lKfkH8%Yi6XvXugUfl#rOB5sgV0O-T^sbh(^Z zRg`;Haws_4VHA;Mv1%)iqG(!IJ*morW2?qtOb`F_dag5*!f;`q`)F)$!{5zyQ?p78 z(dLCI&CPgK%%Okq+FH+C0Tmm$twf1Y|EV%gR__(7VJ_89VqG literal 0 HcmV?d00001 diff --git a/dashboard/horizon/locale/pt_BR/LC_MESSAGES/djangojs.po b/dashboard/horizon/locale/pt_BR/LC_MESSAGES/djangojs.po new file mode 100644 index 00000000..559b85f2 --- /dev/null +++ b/dashboard/horizon/locale/pt_BR/LC_MESSAGES/djangojs.po @@ -0,0 +1,74 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:08+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" + +#: static/horizon/js/horizon.forms.js:47 +msgid "Additional information here..." +msgstr "" + +#: static/horizon/js/horizon.forms.js:53 +msgid "Filter" +msgstr "" + +#: static/horizon/js/horizon.instances.js:28 +msgid "There was a problem communicating with the server, please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:125 +msgid "There was an error submitting the form. Please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:159 static/horizon/js/horizon.tabs.js:9 +msgid "Loading" +msgstr "" + +#: static/horizon/js/horizon.modals.js:178 +msgid "An error occurred. Please try again." +msgstr "" + +#: static/horizon/js/horizon.tables.js:47 +msgid "An error occurred while updating." +msgstr "" + +#: static/horizon/js/horizon.tables.js:145 +msgid "You have selected " +msgstr "" + +#: static/horizon/js/horizon.tables.js:158 +msgid "Confirm " +msgstr "" + +#: static/horizon/js/horizon.tables.js:159 +msgid "Please confirm your selection. This action cannot be undone." +msgstr "" + +#: static/horizon/js/horizon.tables.js:173 +msgid "Working" +msgstr "" + +#: static/horizon/js/horizon.tables.js:216 +#, c-format +msgid "Displaying %s item" +msgid_plural "Displaying %s items" +msgstr[0] "" +msgstr[1] "" + +#: static/horizon/js/horizon.users.js:18 +msgid "Passwords do not match." +msgstr "" diff --git a/dashboard/horizon/locale/ru/LC_MESSAGES/django.mo b/dashboard/horizon/locale/ru/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..87056ea31e794dc0a6feb1edba99526c143be1a9 GIT binary patch literal 585 zcmYk3&2H2%5XS?8OOBj53_@E`ffqY`XzNW%OWA%%Sy-wRiKEH7X-d|PYzJu7OUoM| z@dTWadMFnp;1!$~;8~b#LCZ*g&5Zr~+hh6njccC+jH{@dsC%gEs2J771a%wr67}^; z5Hy&3-5MIOo%=8?;En+p*ni2k0E}q_5PZ5x}M3tl5RCjg(@0RNRml0 z8bO?OcRcG#-qi%Sf7R)S~;okx}zHBcJhO}Uss}3L&y(= zvC<`b*(Mn*xNfX{X~;=SCi=9R-8QS%DIEajKayur3D$fj2?!bXn7b0{tNg|{=7W}|HFT4 X&%j^!pY3UT)_(NA{5kmV-Jkve7c;1L literal 0 HcmV?d00001 diff --git a/dashboard/horizon/locale/ru/LC_MESSAGES/django.po b/dashboard/horizon/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 00000000..ecbb78b7 --- /dev/null +++ b/dashboard/horizon/locale/ru/LC_MESSAGES/django.po @@ -0,0 +1,517 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# , 2012. +msgid "" +msgstr "" +"Project-Id-Version: Horizon\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:08+0000\n" +"PO-Revision-Date: 2012-08-16 08:19+0000\n" +"Last-Translator: lykoz \n" +"Language-Team: LANGUAGE \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#: base.py:424 +msgid "Other" +msgstr "" + +#: decorators.py:55 +msgid "Please log in to continue." +msgstr "" + +#: decorators.py:87 +#, python-format +msgid "You are not authorized to access %s" +msgstr "У Вас нет доступа к %s" + +#: exceptions.py:283 +msgid "Unauthorized. Please try logging in again." +msgstr "" + +#: browsers/base.py:90 +msgid "Navigation Item" +msgstr "" + +#: browsers/views.py:42 +#, python-format +msgid "Select a %s to browse." +msgstr "" + +#: conf/default.py:29 +msgid "Password is not accepted" +msgstr "" + +#: tables/actions.py:349 +msgid "Filter" +msgstr "" + +#: tables/actions.py:527 +#, python-format +msgid "%(action)s %(data_type)s" +msgstr "" + +#: tables/actions.py:561 +msgid "N/A" +msgstr "" + +#: tables/actions.py:589 +#, python-format +msgid "You do not have permission to %(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:595 +#, python-format +msgid "Unable to %(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:601 +#, python-format +msgid "%(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:611 +msgid "Delete" +msgstr "" + +#: tables/actions.py:612 +msgid "Deleted" +msgstr "" + +#: tables/base.py:275 +#, python-format +msgid "The attribute %(attr)s doesn't exist on %(obj)s." +msgstr "" + +#: tables/base.py:748 +msgid "No items to display." +msgstr "" + +#: tables/base.py:852 +msgid "Actions" +msgstr "" + +#: tables/base.py:1035 +#, python-format +msgid "No match returned for the id \"%s\"." +msgstr "" + +#: tables/base.py:1165 +msgid "Please select a row before taking that action." +msgstr "" + +#: templates/_header.html:3 +msgid "Logged in as" +msgstr "" + +#: templates/_header.html:5 +msgid "Help" +msgstr "" + +#: templates/_header.html:7 +msgid "Sign Out" +msgstr "" + +#: templates/splash.html:7 templates/auth/login.html:4 +msgid "Login" +msgstr "" + +#: templates/auth/_login.html:4 +msgid "Log In" +msgstr "" + +#: templates/auth/_login.html:14 +msgid "You don't have permissions to access:" +msgstr "" + +#: templates/auth/_login.html:16 +msgid "Login as different user or go back to" +msgstr "" + +#: templates/auth/_login.html:17 +msgid "home page" +msgstr "" + +#: templates/auth/_login.html:27 +msgid "Sign In" +msgstr "" + +#: templates/horizon/_messages.html:7 +msgid "Info: " +msgstr "" + +#: templates/horizon/_messages.html:13 +msgid "Warning: " +msgstr "" + +#: templates/horizon/_messages.html:19 +msgid "Success: " +msgstr "" + +#: templates/horizon/_messages.html:25 +msgid "Error: " +msgstr "" + +#: templates/horizon/common/_data_table.html:54 +msgid "Summary" +msgstr "" + +#: templates/horizon/common/_data_table.html:63 +#, python-format +msgid "Displaying %(counter)s item" +msgid_plural "Displaying %(counter)s items" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: templates/horizon/common/_data_table_row_actions.html:10 +msgid "More" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:4 +msgid "Quota Summary" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "Used" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "of" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +msgid "Available Instances" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:8 +msgid "Available vCPUs" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:11 +msgid "Available RAM" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:15 +msgid "Available volumes" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:18 +msgid "Available volume storage" +msgstr "" + +#: templates/horizon/common/_resource_browser.html:10 +#, python-format +msgid "Displaying %(nav_items)s item" +msgid_plural "Displaying %(nav_items)s items" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: templates/horizon/common/_resource_browser.html:11 +#, python-format +msgid "Displaying %(content_items)s item" +msgid_plural "Displaying %(content_items)s items" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: templates/horizon/common/_sidebar.html:14 +msgid "Current Project" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:5 +msgid "Select a month to query its usage" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:9 +msgid "Submit" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:14 +msgid "Active Instances" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:15 +msgid "Active RAM" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:16 +msgid "This Month's VCPU-Hours" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:17 +msgid "This Month's GB-Hours" +msgstr "" + +#: templates/horizon/common/_workflow.html:33 +msgid "Cancel" +msgstr "" + +#: templatetags/branding.py:35 +msgid "Horizon" +msgstr "" + +#: templatetags/horizon.py:109 +msgid "No Limit" +msgstr "" + +#: templatetags/horizon.py:111 templatetags/horizon.py:113 +msgid "Available" +msgstr "" + +#: templatetags/sizeformat.py:45 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: templatetags/sizeformat.py:49 +#, python-format +msgid "%(size)d" +msgid_plural "%(size)d" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: templatetags/sizeformat.py:52 +#, python-format +msgid "%s KB" +msgstr "" + +#: templatetags/sizeformat.py:55 +#, python-format +msgid "%s MB" +msgstr "" + +#: templatetags/sizeformat.py:58 +#, python-format +msgid "%s GB" +msgstr "" + +#: templatetags/sizeformat.py:61 +#, python-format +msgid "%s TB" +msgstr "" + +#: templatetags/sizeformat.py:63 +#, python-format +msgid "%s PB" +msgstr "" + +#: test/settings.py:114 +msgid "Password must be between 8 and 18 characters." +msgstr "" + +#: test/test_dashboards/cats/dashboard.py:8 +msgid "Cute Cats" +msgstr "" + +#: test/test_dashboards/cats/dashboard.py:14 +msgid "Fierce Cats" +msgstr "" + +#: test/test_dashboards/cats/dashboard.py:19 +msgid "Cats" +msgstr "" + +#: test/test_dashboards/cats/kittens/panel.py:9 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:3 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:6 +msgid "Kittens" +msgstr "" + +#: test/test_dashboards/cats/tigers/panel.py:9 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:3 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:6 +msgid "Tigers" +msgstr "" + +#: test/test_dashboards/dogs/dashboard.py:7 +msgid "Dogs" +msgstr "" + +#: test/test_dashboards/dogs/puppies/panel.py:9 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:3 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:6 +msgid "Puppies" +msgstr "" + +#: test/tests/base.py:39 +msgid "My Dashboard" +msgstr "" + +#: test/tests/base.py:45 +msgid "My Panel" +msgstr "" + +#: test/tests/base.py:51 +msgid "Admin Panel" +msgstr "" + +#: test/tests/messages.py:32 +msgid "Giant ants are attacking San Francisco!" +msgstr "" + +#: test/tests/messages.py:46 +msgid "We are now safe from ants! Go here!" +msgstr "" + +#: test/tests/tables.py:107 +msgid "Batch" +msgstr "" + +#: test/tests/tables.py:108 +msgid "Batched" +msgstr "" + +#: test/tests/tables.py:109 test/tests/tables.py:120 +msgid "Item" +msgstr "" + +#: test/tests/tables.py:110 test/tests/tables.py:121 +msgid "Items" +msgstr "" + +#: test/tests/tables.py:118 +msgid "Down" +msgstr "" + +#: test/tests/tables.py:118 +msgid "Up" +msgstr "" + +#: test/tests/tables.py:119 +msgid "Downed" +msgstr "" + +#: test/tests/tables.py:119 +msgid "Upped" +msgstr "" + +#: test/tests/tables.py:187 +msgid "No Actions Table" +msgstr "" + +#: test/tests/tables.py:684 +msgid "Single Table" +msgstr "" + +#: test/tests/tabs.py:36 +msgid "Tab One" +msgstr "" + +#: test/tests/tabs.py:42 +msgid "Delayed Tab" +msgstr "" + +#: test/tests/tabs.py:49 +msgid "Disabled Tab" +msgstr "" + +#: test/tests/tabs.py:58 +msgid "Disallowed Tab" +msgstr "" + +#: test/tests/tabs.py:76 +msgid "Tab With My Table" +msgstr "" + +#: test/tests/tabs.py:85 +msgid "Recoverable Error Tab" +msgstr "" + +#: test/tests/workflows.py:43 +msgid "Project" +msgstr "" + +#: test/tests/workflows.py:44 +msgid "User" +msgstr "" + +#: test/tests/workflows.py:47 +msgid "Test Action One" +msgstr "" + +#: test/tests/workflows.py:61 +msgid "Instance" +msgstr "" + +#: test/tests/workflows.py:64 +msgid "Test Action Two" +msgstr "" + +#: test/tests/workflows.py:72 +msgid "Test Action Three" +msgstr "" + +#: test/tests/workflows.py:77 +msgid "Admin" +msgstr "" + +#: test/tests/workflows.py:80 +msgid "Admin Action" +msgstr "" + +#: utils/fields.py:46 +msgid "Incorrect format for IP address" +msgstr "" + +#: utils/fields.py:47 +msgid "Invalid version for IP address" +msgstr "" + +#: utils/fields.py:48 +msgid "Invalid subnet mask" +msgstr "" + +#: workflows/base.py:71 +msgid "Processing..." +msgstr "" + +#: workflows/base.py:467 +msgid "All available" +msgstr "" + +#: workflows/base.py:468 +msgid "Members" +msgstr "" + +#: workflows/base.py:469 +msgid "None available." +msgstr "" + +#: workflows/base.py:470 +msgid "No members." +msgstr "" + +#: workflows/base.py:569 +msgid "Save" +msgstr "" + +#: workflows/base.py:570 +#, python-format +msgid "%s completed successfully." +msgstr "" + +#: workflows/base.py:571 +#, python-format +msgid "%s did not complete." +msgstr "" diff --git a/dashboard/horizon/locale/ru/LC_MESSAGES/djangojs.mo b/dashboard/horizon/locale/ru/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000000000000000000000000000000000000..ca330b2bc94d8b0006277ec0e6a9952e7daa129d GIT binary patch literal 494 zcmYL^(N5bi6ox_LDwkaE;!GN72oAF|kXq9Kp>ui8daM!SbSHm)jpb+W9)6XF4moPD9yqyYf}m6-PS&sHfyIE99wta3NWkZ zv5h{R!Al!`FngRu5r_SK=vwwJ;v6o30pT>CN8!KKAUf&VX9pW_29J*~7Gl@|c{q8( HgJA0~4j_Y4 literal 0 HcmV?d00001 diff --git a/dashboard/horizon/locale/ru/LC_MESSAGES/djangojs.po b/dashboard/horizon/locale/ru/LC_MESSAGES/djangojs.po new file mode 100644 index 00000000..9a5f3639 --- /dev/null +++ b/dashboard/horizon/locale/ru/LC_MESSAGES/djangojs.po @@ -0,0 +1,75 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:08+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" + +#: static/horizon/js/horizon.forms.js:47 +msgid "Additional information here..." +msgstr "" + +#: static/horizon/js/horizon.forms.js:53 +msgid "Filter" +msgstr "" + +#: static/horizon/js/horizon.instances.js:28 +msgid "There was a problem communicating with the server, please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:125 +msgid "There was an error submitting the form. Please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:159 static/horizon/js/horizon.tabs.js:9 +msgid "Loading" +msgstr "" + +#: static/horizon/js/horizon.modals.js:178 +msgid "An error occurred. Please try again." +msgstr "" + +#: static/horizon/js/horizon.tables.js:47 +msgid "An error occurred while updating." +msgstr "" + +#: static/horizon/js/horizon.tables.js:145 +msgid "You have selected " +msgstr "" + +#: static/horizon/js/horizon.tables.js:158 +msgid "Confirm " +msgstr "" + +#: static/horizon/js/horizon.tables.js:159 +msgid "Please confirm your selection. This action cannot be undone." +msgstr "" + +#: static/horizon/js/horizon.tables.js:173 +msgid "Working" +msgstr "" + +#: static/horizon/js/horizon.tables.js:216 +#, c-format +msgid "Displaying %s item" +msgid_plural "Displaying %s items" +msgstr[0] "" +msgstr[1] "" + +#: static/horizon/js/horizon.users.js:18 +msgid "Passwords do not match." +msgstr "" diff --git a/dashboard/horizon/locale/zh_CN/LC_MESSAGES/django.mo b/dashboard/horizon/locale/zh_CN/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..71221e530ed8f3305f36e593b6147740344d0649 GIT binary patch literal 875 zcmYk4-%C_M6vxN1UkklRKcIS;h+c}>?p+Y{s-bOa!m69DK_SF=ce;D??#wcGw$i7~ zs1zxE$q*_C6fJ74C>0j!KZ&Bx%@l#uQu zONQ~5kCGGG%W-+lk2=)lVRTt!F_jE+0XF|OX)|iu%=f|hZ#UAHB$t@>6R51%j3^PJZgBZ=&Y_%e-rws@wBtz_m zt4;r}Gg^rm!A8U~n(K8j>MN3vl=~i4LR$D@mJg>~d?iKUKJAlQ!sF8QJDoIjy|69P zXG1YaW~swSg3+{vV_gHiBVE1SxIGr@^xUwJ&UTH3CsS9W1EK!D{_gE-nqeapg=c3R zx=&=_8j9UgXEGiF#>5uz)IS8|B49b@5(l{$aVWQZBqk zrH$qC>MW|}W`fxl#m!uC^CkQP!H2oZ$~3Cvmnv(wOG|Gn@0Y*jreJ&Z{)1rQP2B`C cD41Fe-aQYV%mh#GR2Szf`A^lydHB?S0J#ec^Z)<= literal 0 HcmV?d00001 diff --git a/dashboard/horizon/locale/zh_CN/LC_MESSAGES/django.po b/dashboard/horizon/locale/zh_CN/LC_MESSAGES/django.po new file mode 100644 index 00000000..56ae0c07 --- /dev/null +++ b/dashboard/horizon/locale/zh_CN/LC_MESSAGES/django.po @@ -0,0 +1,511 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# , 2012. +# QunShi Zhang , 2012. +# yuanke wei , 2012. +msgid "" +msgstr "" +"Project-Id-Version: Horizon\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:08+0000\n" +"PO-Revision-Date: 2012-09-12 02:05+0000\n" +"Last-Translator: QunShi Zhang \n" +"Language-Team: LANGUAGE \n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: base.py:424 +msgid "Other" +msgstr "其他" + +#: decorators.py:55 +msgid "Please log in to continue." +msgstr "请登陆后继续" + +#: decorators.py:87 +#, python-format +msgid "You are not authorized to access %s" +msgstr "您没有权限访问%s" + +#: exceptions.py:283 +msgid "Unauthorized. Please try logging in again." +msgstr "认证失败。请重新登陆。" + +#: browsers/base.py:90 +msgid "Navigation Item" +msgstr "" + +#: browsers/views.py:42 +#, python-format +msgid "Select a %s to browse." +msgstr "选择一个%s 浏览" + +#: conf/default.py:29 +msgid "Password is not accepted" +msgstr "" + +#: tables/actions.py:349 +msgid "Filter" +msgstr "" + +#: tables/actions.py:527 +#, python-format +msgid "%(action)s %(data_type)s" +msgstr "" + +#: tables/actions.py:561 +msgid "N/A" +msgstr "" + +#: tables/actions.py:589 +#, python-format +msgid "You do not have permission to %(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:595 +#, python-format +msgid "Unable to %(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:601 +#, python-format +msgid "%(action)s: %(objs)s" +msgstr "" + +#: tables/actions.py:611 +msgid "Delete" +msgstr "" + +#: tables/actions.py:612 +msgid "Deleted" +msgstr "" + +#: tables/base.py:275 +#, python-format +msgid "The attribute %(attr)s doesn't exist on %(obj)s." +msgstr "" + +#: tables/base.py:748 +msgid "No items to display." +msgstr "" + +#: tables/base.py:852 +msgid "Actions" +msgstr "" + +#: tables/base.py:1035 +#, python-format +msgid "No match returned for the id \"%s\"." +msgstr "" + +#: tables/base.py:1165 +msgid "Please select a row before taking that action." +msgstr "" + +#: templates/_header.html:3 +msgid "Logged in as" +msgstr "" + +#: templates/_header.html:5 +msgid "Help" +msgstr "" + +#: templates/_header.html:7 +msgid "Sign Out" +msgstr "" + +#: templates/splash.html:7 templates/auth/login.html:4 +msgid "Login" +msgstr "" + +#: templates/auth/_login.html:4 +msgid "Log In" +msgstr "" + +#: templates/auth/_login.html:14 +msgid "You don't have permissions to access:" +msgstr "" + +#: templates/auth/_login.html:16 +msgid "Login as different user or go back to" +msgstr "" + +#: templates/auth/_login.html:17 +msgid "home page" +msgstr "" + +#: templates/auth/_login.html:27 +msgid "Sign In" +msgstr "" + +#: templates/horizon/_messages.html:7 +msgid "Info: " +msgstr "" + +#: templates/horizon/_messages.html:13 +msgid "Warning: " +msgstr "" + +#: templates/horizon/_messages.html:19 +msgid "Success: " +msgstr "" + +#: templates/horizon/_messages.html:25 +msgid "Error: " +msgstr "" + +#: templates/horizon/common/_data_table.html:54 +msgid "Summary" +msgstr "" + +#: templates/horizon/common/_data_table.html:63 +#, python-format +msgid "Displaying %(counter)s item" +msgid_plural "Displaying %(counter)s items" +msgstr[0] "" + +#: templates/horizon/common/_data_table_row_actions.html:10 +msgid "More" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:4 +msgid "Quota Summary" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "Used" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "of" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#, fuzzy +msgid "Available Instances" +msgstr "实例" + +#: templates/horizon/common/_quota_summary.html:8 +msgid "Available vCPUs" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:11 +msgid "Available RAM" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:15 +#, fuzzy +msgid "Available volumes" +msgstr "实例" + +#: templates/horizon/common/_quota_summary.html:18 +#, fuzzy +msgid "Available volume storage" +msgstr "实例" + +#: templates/horizon/common/_resource_browser.html:10 +#, python-format +msgid "Displaying %(nav_items)s item" +msgid_plural "Displaying %(nav_items)s items" +msgstr[0] "" + +#: templates/horizon/common/_resource_browser.html:11 +#, python-format +msgid "Displaying %(content_items)s item" +msgid_plural "Displaying %(content_items)s items" +msgstr[0] "" + +#: templates/horizon/common/_sidebar.html:14 +msgid "Current Project" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:5 +msgid "Select a month to query its usage" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:9 +msgid "Submit" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:14 +msgid "Active Instances" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:15 +msgid "Active RAM" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:16 +msgid "This Month's VCPU-Hours" +msgstr "" + +#: templates/horizon/common/_usage_summary.html:17 +msgid "This Month's GB-Hours" +msgstr "" + +#: templates/horizon/common/_workflow.html:33 +msgid "Cancel" +msgstr "取消" + +#: templatetags/branding.py:35 +msgid "Horizon" +msgstr "" + +#: templatetags/horizon.py:109 +msgid "No Limit" +msgstr "" + +#: templatetags/horizon.py:111 templatetags/horizon.py:113 +msgid "Available" +msgstr "" + +#: templatetags/sizeformat.py:45 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "" + +#: templatetags/sizeformat.py:49 +#, python-format +msgid "%(size)d" +msgid_plural "%(size)d" +msgstr[0] "" + +#: templatetags/sizeformat.py:52 +#, python-format +msgid "%s KB" +msgstr "" + +#: templatetags/sizeformat.py:55 +#, python-format +msgid "%s MB" +msgstr "" + +#: templatetags/sizeformat.py:58 +#, python-format +msgid "%s GB" +msgstr "" + +#: templatetags/sizeformat.py:61 +#, python-format +msgid "%s TB" +msgstr "" + +#: templatetags/sizeformat.py:63 +#, python-format +msgid "%s PB" +msgstr "" + +#: test/settings.py:114 +msgid "Password must be between 8 and 18 characters." +msgstr "" + +#: test/test_dashboards/cats/dashboard.py:8 +msgid "Cute Cats" +msgstr "" + +#: test/test_dashboards/cats/dashboard.py:14 +msgid "Fierce Cats" +msgstr "" + +#: test/test_dashboards/cats/dashboard.py:19 +msgid "Cats" +msgstr "" + +#: test/test_dashboards/cats/kittens/panel.py:9 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:3 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:6 +msgid "Kittens" +msgstr "" + +#: test/test_dashboards/cats/tigers/panel.py:9 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:3 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:6 +msgid "Tigers" +msgstr "" + +#: test/test_dashboards/dogs/dashboard.py:7 +msgid "Dogs" +msgstr "" + +#: test/test_dashboards/dogs/puppies/panel.py:9 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:3 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:6 +msgid "Puppies" +msgstr "" + +#: test/tests/base.py:39 +msgid "My Dashboard" +msgstr "" + +#: test/tests/base.py:45 +msgid "My Panel" +msgstr "" + +#: test/tests/base.py:51 +msgid "Admin Panel" +msgstr "" + +#: test/tests/messages.py:32 +msgid "Giant ants are attacking San Francisco!" +msgstr "" + +#: test/tests/messages.py:46 +msgid "We are now safe from ants! Go here!" +msgstr "" + +#: test/tests/tables.py:107 +msgid "Batch" +msgstr "" + +#: test/tests/tables.py:108 +msgid "Batched" +msgstr "" + +#: test/tests/tables.py:109 test/tests/tables.py:120 +msgid "Item" +msgstr "" + +#: test/tests/tables.py:110 test/tests/tables.py:121 +msgid "Items" +msgstr "" + +#: test/tests/tables.py:118 +msgid "Down" +msgstr "" + +#: test/tests/tables.py:118 +msgid "Up" +msgstr "" + +#: test/tests/tables.py:119 +msgid "Downed" +msgstr "" + +#: test/tests/tables.py:119 +msgid "Upped" +msgstr "" + +#: test/tests/tables.py:187 +msgid "No Actions Table" +msgstr "" + +#: test/tests/tables.py:684 +msgid "Single Table" +msgstr "" + +#: test/tests/tabs.py:36 +msgid "Tab One" +msgstr "" + +#: test/tests/tabs.py:42 +msgid "Delayed Tab" +msgstr "" + +#: test/tests/tabs.py:49 +msgid "Disabled Tab" +msgstr "" + +#: test/tests/tabs.py:58 +msgid "Disallowed Tab" +msgstr "" + +#: test/tests/tabs.py:76 +msgid "Tab With My Table" +msgstr "" + +#: test/tests/tabs.py:85 +msgid "Recoverable Error Tab" +msgstr "" + +#: test/tests/workflows.py:43 +msgid "Project" +msgstr "工程" + +#: test/tests/workflows.py:44 +msgid "User" +msgstr "" + +#: test/tests/workflows.py:47 +msgid "Test Action One" +msgstr "" + +#: test/tests/workflows.py:61 +msgid "Instance" +msgstr "实例" + +#: test/tests/workflows.py:64 +msgid "Test Action Two" +msgstr "" + +#: test/tests/workflows.py:72 +msgid "Test Action Three" +msgstr "" + +#: test/tests/workflows.py:77 +msgid "Admin" +msgstr "" + +#: test/tests/workflows.py:80 +msgid "Admin Action" +msgstr "" + +#: utils/fields.py:46 +msgid "Incorrect format for IP address" +msgstr "" + +#: utils/fields.py:47 +msgid "Invalid version for IP address" +msgstr "" + +#: utils/fields.py:48 +msgid "Invalid subnet mask" +msgstr "" + +#: workflows/base.py:71 +msgid "Processing..." +msgstr "" + +#: workflows/base.py:467 +msgid "All available" +msgstr "" + +#: workflows/base.py:468 +msgid "Members" +msgstr "" + +#: workflows/base.py:469 +msgid "None available." +msgstr "" + +#: workflows/base.py:470 +msgid "No members." +msgstr "" + +#: workflows/base.py:569 +msgid "Save" +msgstr "" + +#: workflows/base.py:570 +#, python-format +msgid "%s completed successfully." +msgstr "" + +#: workflows/base.py:571 +#, python-format +msgid "%s did not complete." +msgstr "" diff --git a/dashboard/horizon/locale/zh_CN/LC_MESSAGES/djangojs.mo b/dashboard/horizon/locale/zh_CN/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000000000000000000000000000000000000..56ad7ecd41fd04ff1ed387d0890e5ee204ecaa53 GIT binary patch literal 413 zcmYL^Pfx-y7>6->+R?Lz9=zd;8;J=NFici(asM&~iQej{b7rw1(1Ke$1Oc~#lA-Gj!E?dABHow|$9)%rBIM$& z?k@lH?wOxMoI>D}+myy({~=A76LDjq&|I2^Tcs&kZlZ`Je$2>}#eNvg{UFHMdVP(m ztoFPTPze4?N#rN1O;>puMf0W}C|w;KtQv, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:08+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0\n" + +#: static/horizon/js/horizon.forms.js:47 +msgid "Additional information here..." +msgstr "" + +#: static/horizon/js/horizon.forms.js:53 +msgid "Filter" +msgstr "" + +#: static/horizon/js/horizon.instances.js:28 +msgid "There was a problem communicating with the server, please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:125 +msgid "There was an error submitting the form. Please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:159 static/horizon/js/horizon.tabs.js:9 +msgid "Loading" +msgstr "" + +#: static/horizon/js/horizon.modals.js:178 +msgid "An error occurred. Please try again." +msgstr "" + +#: static/horizon/js/horizon.tables.js:47 +msgid "An error occurred while updating." +msgstr "" + +#: static/horizon/js/horizon.tables.js:145 +msgid "You have selected " +msgstr "" + +#: static/horizon/js/horizon.tables.js:158 +msgid "Confirm " +msgstr "" + +#: static/horizon/js/horizon.tables.js:159 +msgid "Please confirm your selection. This action cannot be undone." +msgstr "" + +#: static/horizon/js/horizon.tables.js:173 +msgid "Working" +msgstr "" + +#: static/horizon/js/horizon.tables.js:216 +#, c-format +msgid "Displaying %s item" +msgid_plural "Displaying %s items" +msgstr[0] "" + +#: static/horizon/js/horizon.users.js:18 +msgid "Passwords do not match." +msgstr "" diff --git a/dashboard/horizon/locale/zh_TW/LC_MESSAGES/django.mo b/dashboard/horizon/locale/zh_TW/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..6ef8c2704baf6edb40e23caf9d8a6cbf65b6d08c GIT binary patch literal 5020 zcmai#TW}P|6^1*QIF91P2AlYj3mqH+0`KaQ7z<%AAaPNUK%f=au5wYM-9|fRcV?NH zl@RfRgaNIPK*CsnKp2EU2+$31gwO@@kmAKjrBantIhRVR!p`h&UYv(Kgoj-IGt-L% z1y0wT`KE8DPoHzT=P$Eob~2PI$af*1&R}d8SpHSKP@er7V=scUz~{k5;4JV>kj8HY zX{aV?N$!zslso{wjB#xs>A48vVH0>g3;q%$J--K^2LA+p15ATo2mdMi{{!M-GoNDY zN$_d#8Spuf#=j!l^FSK61SEYcz;A&Yz*6uXun0T@z5#v?&IIRRF`D->Nc!i2)SfHb z{oq{Gm&pFDAf~c+!RNpo;P=5jAkFInXgMRQF_)YM)Anot3;2baolAZqszYWfS z6Mq1{1d=_=K(f0O#L{d7Nb{>D-vucj1W5MQ%X+h{cY*n6zXZ~He*|9u{{fPpo`N$L zfeS#S9@_$vJ=Gws_YU|97y=8y1bFc)jE#e|-yniYdfx}h-Vedq;3<&&GAQ|}QTnW;+b+W!0#KYdgi{^!8dmN;3 z@5}ZBARcxUFN)_UAf2l_Ao=GmNck`WNq`WsCqY_wmgI{d#bYi=`&aUSXR`hqko@&$kmh{>()#y6Iv=x<6tqqu zNaL1*ey|)QJAMW(0FQvA|0YP|@5ugN%lciA^!^>B@lQY*jaw{P2-1F+%6chC_G|$0 zuxh;ELKc?XEtvr6oSXphuse8>-G2b-+}r~x9?#=Y)4pE^N$)a{)>{M8IjID5K?6iu zvQ-c&n>?`ZNzxs&9@f2>FM~Ymg0)3JCe?$B=AUh>tfRboL)EE73r{rcw-{e8`qO`LPtF`-wxUAe0j& z5GpvLZp`SLa*Xbebr3oiR4O4qgsh+n%6B}lB_QQgw$OQF^A{>XOVzbplh0omQY>Y= z)esZ8=CqDt^yoG3nQo)0?h&~m^gi^#YZ@$ZzuB!phsoEiruPQ-UE#hf-FLu!2lZ$y zA}kT&W;_@arnw^?i8T0WN=OZHO}BVJ&Ai)IZkwp)zt8E7xmznMSCEZ&i&eZW}^e z(ZEX;GhCx9Mu;k2d^hH;VjGn@wN{}-=A?ve)MX}^6i0*}4drT7wdli*3b%CRm>Us4 zHDX)AFgJu1H#96u=2>CEv5R^0&3S&d2{nUNDyCVl8!*dsvr|FV#*n{L9nrXH@fv~0 zsux1zMO@KByr75&!-|2khVA=VWke{Z;1TQ;`-3HvFsc?8_q$AF9{jCvL$8M}m?OBQ z?4D-{*?{{l%4sQ`b9ge1*b2qSVHdiTif>mV|n+lq+C^W{zgogAz`enjpEYvr-dPU3S z3&WNbE6&TSudnx8q+8t~cGJ|nn0F)02-udSF{Vz-rtn^@JBxrfpe*7EAWT3^xBII?1gFnnup@rB%| z7S*U$PGux+C=uUUT!LmX*J5tfTwbt@d!Nhmm*udjghFmT`j7g>#L371Qh9RnkiGwd z)X(?*|Np0bG`un(R|o!n;+w&DA3<6fF(51(|pFHbeut)0oqZkB0k1(Kr!_L&>$ zo;JI&HFM=7mKy9ybsR`H?w4P3u-BY9ea<<3o7wF{sgL@Y({j`4?_spA{Axeuj3ij< z)S%sRh^!b+I6VnwkB?-UuGxtLnX^sIPF&7(ou^6JucmG&lfB7NY_}=hcU9IiJwvJU zWANR>KT)#w?0&4orVYvd1|QKm{YN*|og8gtnYN+y<@1OFXHG}AJ>1Hit9$K(m!^Jr zm2MPHH#%o0?1^@m=nQ<4Iyc7B*PGLQ2i=(ux^Q2Q)0v?C4|JqPuBC4bWts+4lb0C& zk`&L=jj5J|)7_tP$Fb}_yzQjA#_jzVn1|HfG5N)u#1Uwm_RI4cWJKt7G8A&R}?^tHVx=Ft0N?KAxQHOr09d9PiG~YPm%bNDRwA9aemx-`+oB z4}QSxp#!ON6BwC3cf~n;!EQe;o9x^BGiMXY@mA;91b%ig_N&ej#;pA~n_~TQg0g&e7)NXd@;${rkvWx7wVRL(;)s=BB$kk)z1mzcuz^ z+g_5j580>0EO1 zHc~Uw-0lXUd5k&j9m%othy@)pFJ+FLfLdfVB{$5Laq?U)jc`W$)0cX^ShzzTsJE~O z`p4RsGjQI%+V3=8%>L<;qrDWC?mqd0OO77Iu282fAd#GI$~OGxoUXl2=jc>Zy6cp4 zwBK`=-PM=vr^LJ7IdwNp``J1D2azNc+L7zf&Jn0g^C9Qhuoo^`5UVrKGilrs>49GR ih<6B_y?roax(4p81aGFgTk)sE2ru{KAlJW-wBvu=HvNwP literal 0 HcmV?d00001 diff --git a/dashboard/horizon/locale/zh_TW/LC_MESSAGES/django.po b/dashboard/horizon/locale/zh_TW/LC_MESSAGES/django.po new file mode 100644 index 00000000..80780717 --- /dev/null +++ b/dashboard/horizon/locale/zh_TW/LC_MESSAGES/django.po @@ -0,0 +1,517 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the Horizon package. +# +# Translators: +# Chao-Hsiung Liao , 2012. +msgid "" +msgstr "" +"Project-Id-Version: Horizon\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:08+0000\n" +"PO-Revision-Date: 2012-08-18 09:21+0000\n" +"Last-Translator: Chao-Hsiung Liao \n" +"Language-Team: Chinese (Taiwan) (http://www.transifex.com/projects/p/" +"openstack/language/zh_TW/)\n" +"Language: zh_TW\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: base.py:424 +msgid "Other" +msgstr "其它" + +#: decorators.py:55 +msgid "Please log in to continue." +msgstr "請登入以繼續。" + +#: decorators.py:87 +#, python-format +msgid "You are not authorized to access %s" +msgstr "您的權限不足 無法查看%s" + +#: exceptions.py:283 +msgid "Unauthorized. Please try logging in again." +msgstr "權限不足。 請重新登入。" + +#: browsers/base.py:90 +msgid "Navigation Item" +msgstr "導覽項目" + +#: browsers/views.py:42 +#, python-format +msgid "Select a %s to browse." +msgstr "選擇要瀏覽的 %s。" + +#: conf/default.py:29 +msgid "Password is not accepted" +msgstr "密碼不被接受" + +#: tables/actions.py:349 +msgid "Filter" +msgstr "搜尋" + +#: tables/actions.py:527 +#, python-format +msgid "%(action)s %(data_type)s" +msgstr "%(action)s %(data_type)s" + +#: tables/actions.py:561 +msgid "N/A" +msgstr "N/A" + +#: tables/actions.py:589 +#, python-format +msgid "You do not have permission to %(action)s: %(objs)s" +msgstr "您沒有權限使用%(action)s: %(objs)s" + +#: tables/actions.py:595 +#, python-format +msgid "Unable to %(action)s: %(objs)s" +msgstr "無法%(action)s: %(objs)s" + +#: tables/actions.py:601 +#, python-format +msgid "%(action)s: %(objs)s" +msgstr "%(action)s:%(objs)s" + +#: tables/actions.py:611 +msgid "Delete" +msgstr "刪除" + +#: tables/actions.py:612 +msgid "Deleted" +msgstr "已刪除" + +#: tables/base.py:275 +#, python-format +msgid "The attribute %(attr)s doesn't exist on %(obj)s." +msgstr "此屬性%(attr)s並不在%(obj)s上存在。" + +#: tables/base.py:748 +msgid "No items to display." +msgstr "沒有任何相關項目" + +#: tables/base.py:852 +msgid "Actions" +msgstr "動作" + +#: tables/base.py:1035 +#, python-format +msgid "No match returned for the id \"%s\"." +msgstr "找不到id \"%s\"的相關項目" + +#: tables/base.py:1165 +msgid "Please select a row before taking that action." +msgstr "請選擇一個欄位後才執行動作" + +#: templates/_header.html:3 +msgid "Logged in as" +msgstr "登入為" + +#: templates/_header.html:5 +msgid "Help" +msgstr "求助" + +#: templates/_header.html:7 +msgid "Sign Out" +msgstr "登出" + +#: templates/splash.html:7 templates/auth/login.html:4 +msgid "Login" +msgstr "登入" + +#: templates/auth/_login.html:4 +msgid "Log In" +msgstr "登入" + +#: templates/auth/_login.html:14 +#, fuzzy +msgid "You don't have permissions to access:" +msgstr "您沒有權限使用%(action)s: %(objs)s" + +#: templates/auth/_login.html:16 +msgid "Login as different user or go back to" +msgstr "" + +#: templates/auth/_login.html:17 +msgid "home page" +msgstr "" + +#: templates/auth/_login.html:27 +msgid "Sign In" +msgstr "" + +#: templates/horizon/_messages.html:7 +msgid "Info: " +msgstr "資訊: " + +#: templates/horizon/_messages.html:13 +msgid "Warning: " +msgstr "警告: " + +#: templates/horizon/_messages.html:19 +msgid "Success: " +msgstr "成功: " + +#: templates/horizon/_messages.html:25 +msgid "Error: " +msgstr "錯誤: " + +#: templates/horizon/common/_data_table.html:54 +msgid "Summary" +msgstr "摘要" + +#: templates/horizon/common/_data_table.html:63 +#, python-format +msgid "Displaying %(counter)s item" +msgid_plural "Displaying %(counter)s items" +msgstr[0] "顯示 %(counter)s 個項目" + +#: templates/horizon/common/_data_table_row_actions.html:10 +msgid "More" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:4 +#, fuzzy +msgid "Quota Summary" +msgstr "摘要" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "Used" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#: templates/horizon/common/_quota_summary.html:8 +#: templates/horizon/common/_quota_summary.html:11 +#: templates/horizon/common/_quota_summary.html:15 +#: templates/horizon/common/_quota_summary.html:18 +msgid "of" +msgstr "" + +#: templates/horizon/common/_quota_summary.html:5 +#, fuzzy +msgid "Available Instances" +msgstr "所有執行個體" + +#: templates/horizon/common/_quota_summary.html:8 +#, fuzzy +msgid "Available vCPUs" +msgstr "可用" + +#: templates/horizon/common/_quota_summary.html:11 +#, fuzzy +msgid "Available RAM" +msgstr "可用" + +#: templates/horizon/common/_quota_summary.html:15 +#, fuzzy +msgid "Available volumes" +msgstr "可用" + +#: templates/horizon/common/_quota_summary.html:18 +#, fuzzy +msgid "Available volume storage" +msgstr "所有執行個體" + +#: templates/horizon/common/_resource_browser.html:10 +#, python-format +msgid "Displaying %(nav_items)s item" +msgid_plural "Displaying %(nav_items)s items" +msgstr[0] "顯示 %(nav_items)s 個項目" + +#: templates/horizon/common/_resource_browser.html:11 +#, python-format +msgid "Displaying %(content_items)s item" +msgid_plural "Displaying %(content_items)s items" +msgstr[0] "顯示 %(content_items)s 個項目" + +#: templates/horizon/common/_sidebar.html:14 +msgid "Current Project" +msgstr "目前的專案" + +#: templates/horizon/common/_usage_summary.html:5 +msgid "Select a month to query its usage" +msgstr "請選擇一個月份以查詢使用量" + +#: templates/horizon/common/_usage_summary.html:9 +msgid "Submit" +msgstr "提交" + +#: templates/horizon/common/_usage_summary.html:14 +msgid "Active Instances" +msgstr "運作中執行個體" + +#: templates/horizon/common/_usage_summary.html:15 +#, fuzzy +msgid "Active RAM" +msgstr "運作中記憶體" + +#: templates/horizon/common/_usage_summary.html:16 +msgid "This Month's VCPU-Hours" +msgstr "本月的虛擬處理器-時數" + +#: templates/horizon/common/_usage_summary.html:17 +msgid "This Month's GB-Hours" +msgstr "本月的GB-時數" + +#: templates/horizon/common/_workflow.html:33 +msgid "Cancel" +msgstr "取消" + +#: templatetags/branding.py:35 +msgid "Horizon" +msgstr "Horizon" + +#: templatetags/horizon.py:109 +msgid "No Limit" +msgstr "不限制" + +#: templatetags/horizon.py:111 templatetags/horizon.py:113 +msgid "Available" +msgstr "可用" + +#: templatetags/sizeformat.py:45 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "%(size)d 位元組" + +#: templatetags/sizeformat.py:49 +#, python-format +msgid "%(size)d" +msgid_plural "%(size)d" +msgstr[0] "%(size)d" + +#: templatetags/sizeformat.py:52 +#, python-format +msgid "%s KB" +msgstr "%s KB" + +#: templatetags/sizeformat.py:55 +#, python-format +msgid "%s MB" +msgstr "%s MB" + +#: templatetags/sizeformat.py:58 +#, python-format +msgid "%s GB" +msgstr "%s GB" + +#: templatetags/sizeformat.py:61 +#, python-format +msgid "%s TB" +msgstr "%s TB" + +#: templatetags/sizeformat.py:63 +#, python-format +msgid "%s PB" +msgstr "%s PB" + +#: test/settings.py:114 +msgid "Password must be between 8 and 18 characters." +msgstr "密碼必須介於 8 到 18 個字元之間。" + +#: test/test_dashboards/cats/dashboard.py:8 +msgid "Cute Cats" +msgstr "" + +#: test/test_dashboards/cats/dashboard.py:14 +msgid "Fierce Cats" +msgstr "" + +#: test/test_dashboards/cats/dashboard.py:19 +msgid "Cats" +msgstr "" + +#: test/test_dashboards/cats/kittens/panel.py:9 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:3 +#: test/test_dashboards/cats/kittens/templates/kittens/index.html:6 +msgid "Kittens" +msgstr "" + +#: test/test_dashboards/cats/tigers/panel.py:9 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:3 +#: test/test_dashboards/cats/tigers/templates/tigers/index.html:6 +msgid "Tigers" +msgstr "" + +#: test/test_dashboards/dogs/dashboard.py:7 +msgid "Dogs" +msgstr "" + +#: test/test_dashboards/dogs/puppies/panel.py:9 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:3 +#: test/test_dashboards/dogs/puppies/templates/puppies/index.html:6 +msgid "Puppies" +msgstr "" + +#: test/tests/base.py:39 +msgid "My Dashboard" +msgstr "我的 Dashboard" + +#: test/tests/base.py:45 +msgid "My Panel" +msgstr "我的面板" + +#: test/tests/base.py:51 +msgid "Admin Panel" +msgstr "管理面板" + +#: test/tests/messages.py:32 +msgid "Giant ants are attacking San Francisco!" +msgstr "" + +#: test/tests/messages.py:46 +msgid "We are now safe from ants! Go here!" +msgstr "" + +#: test/tests/tables.py:107 +msgid "Batch" +msgstr "批次" + +#: test/tests/tables.py:108 +msgid "Batched" +msgstr "已批次" + +#: test/tests/tables.py:109 test/tests/tables.py:120 +msgid "Item" +msgstr "項目" + +#: test/tests/tables.py:110 test/tests/tables.py:121 +msgid "Items" +msgstr "項目" + +#: test/tests/tables.py:118 +msgid "Down" +msgstr "下" + +#: test/tests/tables.py:118 +msgid "Up" +msgstr "上" + +#: test/tests/tables.py:119 +msgid "Downed" +msgstr "" + +#: test/tests/tables.py:119 +msgid "Upped" +msgstr "" + +#: test/tests/tables.py:187 +msgid "No Actions Table" +msgstr "沒有動作表格" + +#: test/tests/tables.py:684 +msgid "Single Table" +msgstr "" + +#: test/tests/tabs.py:36 +msgid "Tab One" +msgstr "分頁一" + +#: test/tests/tabs.py:42 +msgid "Delayed Tab" +msgstr "延遲分頁" + +#: test/tests/tabs.py:49 +msgid "Disabled Tab" +msgstr "停用分頁" + +#: test/tests/tabs.py:58 +msgid "Disallowed Tab" +msgstr "不允許分頁" + +#: test/tests/tabs.py:76 +msgid "Tab With My Table" +msgstr "我的表格分頁" + +#: test/tests/tabs.py:85 +msgid "Recoverable Error Tab" +msgstr "可回復錯誤分頁" + +#: test/tests/workflows.py:43 +msgid "Project" +msgstr "專案" + +#: test/tests/workflows.py:44 +msgid "User" +msgstr "使用者" + +#: test/tests/workflows.py:47 +msgid "Test Action One" +msgstr "測試動作一" + +#: test/tests/workflows.py:61 +msgid "Instance" +msgstr "執行個體" + +#: test/tests/workflows.py:64 +msgid "Test Action Two" +msgstr "測試動作二" + +#: test/tests/workflows.py:72 +msgid "Test Action Three" +msgstr "測試動作三" + +#: test/tests/workflows.py:77 +msgid "Admin" +msgstr "管理者" + +#: test/tests/workflows.py:80 +msgid "Admin Action" +msgstr "管理動作" + +#: utils/fields.py:46 +msgid "Incorrect format for IP address" +msgstr "IP 位址格式不正確" + +#: utils/fields.py:47 +msgid "Invalid version for IP address" +msgstr "IP 位址版本無效" + +#: utils/fields.py:48 +msgid "Invalid subnet mask" +msgstr "無效的子網路遮罩" + +#: workflows/base.py:71 +msgid "Processing..." +msgstr "處理中…" + +#: workflows/base.py:467 +#, fuzzy +msgid "All available" +msgstr "可用" + +#: workflows/base.py:468 +msgid "Members" +msgstr "" + +#: workflows/base.py:469 +#, fuzzy +msgid "None available." +msgstr "可用" + +#: workflows/base.py:470 +msgid "No members." +msgstr "" + +#: workflows/base.py:569 +msgid "Save" +msgstr "儲存" + +#: workflows/base.py:570 +#, python-format +msgid "%s completed successfully." +msgstr "%s 已成功的完成。" + +#: workflows/base.py:571 +#, python-format +msgid "%s did not complete." +msgstr "%s 尚未完成。" diff --git a/dashboard/horizon/locale/zh_TW/LC_MESSAGES/djangojs.mo b/dashboard/horizon/locale/zh_TW/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000000000000000000000000000000000000..56ad7ecd41fd04ff1ed387d0890e5ee204ecaa53 GIT binary patch literal 413 zcmYL^Pfx-y7>6->+R?Lz9=zd;8;J=NFici(asM&~iQej{b7rw1(1Ke$1Oc~#lA-Gj!E?dABHow|$9)%rBIM$& z?k@lH?wOxMoI>D}+myy({~=A76LDjq&|I2^Tcs&kZlZ`Je$2>}#eNvg{UFHMdVP(m ztoFPTPze4?N#rN1O;>puMf0W}C|w;KtQv, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-03-12 04:09+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0\n" + +#: static/horizon/js/horizon.forms.js:47 +msgid "Additional information here..." +msgstr "" + +#: static/horizon/js/horizon.forms.js:53 +msgid "Filter" +msgstr "" + +#: static/horizon/js/horizon.instances.js:28 +msgid "There was a problem communicating with the server, please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:125 +msgid "There was an error submitting the form. Please try again." +msgstr "" + +#: static/horizon/js/horizon.modals.js:159 static/horizon/js/horizon.tabs.js:9 +msgid "Loading" +msgstr "" + +#: static/horizon/js/horizon.modals.js:178 +msgid "An error occurred. Please try again." +msgstr "" + +#: static/horizon/js/horizon.tables.js:47 +msgid "An error occurred while updating." +msgstr "" + +#: static/horizon/js/horizon.tables.js:145 +msgid "You have selected " +msgstr "" + +#: static/horizon/js/horizon.tables.js:158 +msgid "Confirm " +msgstr "" + +#: static/horizon/js/horizon.tables.js:159 +msgid "Please confirm your selection. This action cannot be undone." +msgstr "" + +#: static/horizon/js/horizon.tables.js:173 +msgid "Working" +msgstr "" + +#: static/horizon/js/horizon.tables.js:216 +#, c-format +msgid "Displaying %s item" +msgid_plural "Displaying %s items" +msgstr[0] "" + +#: static/horizon/js/horizon.users.js:18 +msgid "Passwords do not match." +msgstr "" diff --git a/dashboard/horizon/management/__init__.py b/dashboard/horizon/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashboard/horizon/management/commands/__init__.py b/dashboard/horizon/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dashboard/horizon/management/commands/startdash.py b/dashboard/horizon/management/commands/startdash.py new file mode 100644 index 00000000..9ffdd20e --- /dev/null +++ b/dashboard/horizon/management/commands/startdash.py @@ -0,0 +1,59 @@ +import glob +from optparse import make_option +import os + +from django.core.management.base import CommandError +from django.core.management.templates import TemplateCommand +from django.utils.importlib import import_module + +import horizon + + +class Command(TemplateCommand): + template = os.path.join(horizon.__path__[0], "conf", "dash_template") + option_list = TemplateCommand.option_list + ( + make_option('--target', + dest='target', + action='store', + default=None, + help='The directory in which the panel ' + 'should be created. Defaults to the ' + 'current directory. The value "auto" ' + 'may also be used to automatically ' + 'create the panel inside the specified ' + 'dashboard module.'),) + help = ("Creates a Django app directory structure for a new dashboard " + "with the given name in the current directory or optionally in " + "the given directory.") + + def handle(self, dash_name=None, **options): + if dash_name is None: + raise CommandError("You must provide a dashboard name.") + + # Use our default template if one isn't specified. + if not options.get("template", None): + options["template"] = self.template + + # We have html templates as well, so make sure those are included. + options["extensions"].extend(["tmpl", "html", "js", "css"]) + + # Check that the app_name cannot be imported. + try: + import_module(dash_name) + except ImportError: + pass + else: + raise CommandError("%r conflicts with the name of an existing " + "Python module and cannot be used as an app " + "name. Please try another name." % dash_name) + + super(Command, self).handle('dash', dash_name, **options) + + target = options.pop("target", None) + if not target: + target = os.path.join(os.curdir, dash_name) + + # Rename our python template files. + file_names = glob.glob(os.path.join(target, "*.py.tmpl")) + for filename in file_names: + os.rename(filename, filename[:-5]) diff --git a/dashboard/horizon/management/commands/startpanel.py b/dashboard/horizon/management/commands/startpanel.py new file mode 100644 index 00000000..7250db71 --- /dev/null +++ b/dashboard/horizon/management/commands/startpanel.py @@ -0,0 +1,97 @@ +import glob +from optparse import make_option +import os + +from django.core.management.base import CommandError +from django.core.management.templates import TemplateCommand +from django.utils.importlib import import_module + +import horizon + + +class Command(TemplateCommand): + args = "[name] [dashboard name] [optional destination directory]" + option_list = TemplateCommand.option_list + ( + make_option('--dashboard', '-d', + dest='dashboard', + action='store', + default=None, + help='The dotted python path to the ' + 'dashboard which this panel will be ' + 'registered with.'), + make_option('--target', + dest='target', + action='store', + default=None, + help='The directory in which the panel ' + 'should be created. Defaults to the ' + 'current directory. The value "auto" ' + 'may also be used to automatically ' + 'create the panel inside the specified ' + 'dashboard module.'),) + template = os.path.join(horizon.__path__[0], "conf", "panel_template") + help = ("Creates a Django app directory structure for a new panel " + "with the given name in the current directory or optionally in " + "the given directory.") + + def handle(self, panel_name=None, **options): + if panel_name is None: + raise CommandError("You must provide a panel name.") + + if options.get('dashboard') is None: + raise CommandError("You must specify the name of the dashboard " + "this panel will be registered with using the " + "-d or --dashboard option.") + + dashboard_path = options.get('dashboard') + dashboard_mod_path = ".".join([dashboard_path, "dashboard"]) + + # Check the the dashboard.py file in the dashboard app can be imported. + # Add the dashboard information to our options to pass along if all + # goes well. + try: + dashboard_mod = import_module(dashboard_mod_path) + options["dash_path"] = dashboard_path + options["dash_name"] = dashboard_path.split(".")[-1] + except ImportError: + raise CommandError("A dashboard.py module could not be imported " + " from the dashboard at %r." + % options.get("dashboard")) + + target = options.pop("target", None) + if target == "auto": + target = os.path.join(os.path.dirname(dashboard_mod.__file__), + panel_name) + if not os.path.exists(target): + try: + os.mkdir(target) + except OSError, exc: + raise CommandError("Unable to create panel directory: %s" + % exc) + + # Use our default template if one isn't specified. + if not options.get("template", None): + options["template"] = self.template + + # We have html templates as well, so make sure those are included. + options["extensions"].extend(["tmpl", "html"]) + + # Check that the app_name cannot be imported. + try: + import_module(panel_name) + except ImportError: + pass + else: + raise CommandError("%r conflicts with the name of an existing " + "Python module and cannot be used as an app " + "name. Please try another name." % panel_name) + + super(Command, self).handle('panel', panel_name, target, **options) + + if not target: + target = os.path.join(os.curdir, panel_name) + + # Rename our python template files. + file_names = glob.glob(os.path.join(target, "*.py.tmpl")) + for filename in file_names: + os.rename(filename, filename[:-5]) diff --git a/dashboard/horizon/messages.py b/dashboard/horizon/messages.py new file mode 100644 index 00000000..05b2ee10 --- /dev/null +++ b/dashboard/horizon/messages.py @@ -0,0 +1,83 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# 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. + +""" +Drop-in replacement for django.contrib.messages which handles Horizon's +messaging needs (e.g. AJAX communication, etc.). +""" + +from django.contrib import messages as _messages +from django.contrib.messages import constants +from django.utils.encoding import force_unicode +from django.utils.safestring import SafeData + + +def add_message(request, level, message, extra_tags='', fail_silently=False): + """ + Attempts to add a message to the request using the 'messages' app. + """ + if request.is_ajax(): + tag = constants.DEFAULT_TAGS[level] + # if message is marked as safe, pass "safe" tag as extra_tags so that + # client can skip HTML escape for the message when rendering + if isinstance(message, SafeData): + extra_tags = extra_tags + ' safe' + request.horizon['async_messages'].append([tag, + force_unicode(message), + extra_tags]) + else: + return _messages.add_message(request, level, message, + extra_tags, fail_silently) + + +def debug(request, message, extra_tags='', fail_silently=False): + """ + Adds a message with the ``DEBUG`` level. + """ + add_message(request, constants.DEBUG, message, extra_tags=extra_tags, + fail_silently=fail_silently) + + +def info(request, message, extra_tags='', fail_silently=False): + """ + Adds a message with the ``INFO`` level. + """ + add_message(request, constants.INFO, message, extra_tags=extra_tags, + fail_silently=fail_silently) + + +def success(request, message, extra_tags='', fail_silently=False): + """ + Adds a message with the ``SUCCESS`` level. + """ + add_message(request, constants.SUCCESS, message, extra_tags=extra_tags, + fail_silently=fail_silently) + + +def warning(request, message, extra_tags='', fail_silently=False): + """ + Adds a message with the ``WARNING`` level. + """ + add_message(request, constants.WARNING, message, extra_tags=extra_tags, + fail_silently=fail_silently) + + +def error(request, message, extra_tags='', fail_silently=False): + """ + Adds a message with the ``ERROR`` level. + """ + add_message(request, constants.ERROR, message, extra_tags=extra_tags, + fail_silently=fail_silently) diff --git a/dashboard/horizon/middleware.py b/dashboard/horizon/middleware.py new file mode 100644 index 00000000..abd0077e --- /dev/null +++ b/dashboard/horizon/middleware.py @@ -0,0 +1,113 @@ +# 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. +""" +Middleware provided and used by Horizon. +""" + +import json +import logging + +from django import http +from django import shortcuts +from django.conf import settings +from django.contrib import messages as django_messages +from django.contrib.auth import REDIRECT_FIELD_NAME +from django.contrib.auth.views import redirect_to_login +from django.utils import timezone +from django.utils.encoding import iri_to_uri + +from horizon import exceptions + + +LOG = logging.getLogger(__name__) + + +class HorizonMiddleware(object): + """ The main Horizon middleware class. Required for use of Horizon. """ + + def process_request(self, request): + """ Adds data necessary for Horizon to function to the request. """ + # Activate timezone handling + tz = request.session.get('django_timezone') + if tz: + timezone.activate(tz) + + request.horizon = {'dashboard': None, + 'panel': None, + 'async_messages': []} + + def process_exception(self, request, exception): + """ + Catches internal Horizon exception classes such as NotAuthorized, + NotFound and Http302 and handles them gracefully. + """ + if isinstance(exception, (exceptions.NotAuthorized, + exceptions.NotAuthenticated)): + auth_url = settings.LOGIN_URL + next_url = iri_to_uri(request.get_full_path()) + if next_url != auth_url: + field_name = REDIRECT_FIELD_NAME + else: + field_name = None + login_url = request.build_absolute_uri(auth_url) + response = redirect_to_login(next_url, login_url=login_url, + redirect_field_name=field_name) + + # TODO(gabriel): Find a way to display an appropriate message to + # the user *on* the login form... + if request.is_ajax(): + response_401 = http.HttpResponse(status=401) + response_401['X-Horizon-Location'] = response['location'] + return response_401 + return response + + # If an internal "NotFound" error gets this far, return a real 404. + if isinstance(exception, exceptions.NotFound): + raise http.Http404(exception) + + if isinstance(exception, exceptions.Http302): + # TODO(gabriel): Find a way to display an appropriate message to + # the user *on* the login form... + return shortcuts.redirect(exception.location) + + def process_response(self, request, response): + """ + Convert HttpResponseRedirect to HttpResponse if request is via ajax + to allow ajax request to redirect url + """ + if request.is_ajax(): + queued_msgs = request.horizon['async_messages'] + if type(response) == http.HttpResponseRedirect: + # Drop our messages back into the session as per usual so they + # don't disappear during the redirect. Not that we explicitly + # use django's messages methods here. + for tag, message, extra_tags in queued_msgs: + getattr(django_messages, tag)(request, message, extra_tags) + redirect_response = http.HttpResponse() + redirect_response['X-Horizon-Location'] = response['location'] + return redirect_response + if queued_msgs: + # TODO(gabriel): When we have an async connection to the + # client (e.g. websockets) this should be pushed to the + # socket queue rather than being sent via a header. + # The header method has notable drawbacks (length limits, + # etc.) and is not meant as a long-term solution. + response['X-Horizon-Messages'] = json.dumps(queued_msgs) + return response diff --git a/dashboard/horizon/models.py b/dashboard/horizon/models.py new file mode 100644 index 00000000..6313a32f --- /dev/null +++ b/dashboard/horizon/models.py @@ -0,0 +1,23 @@ +# 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. + +""" +Stub file to work around django bug: https://code.djangoproject.com/ticket/7198 +""" diff --git a/dashboard/horizon/site_urls.py b/dashboard/horizon/site_urls.py new file mode 100644 index 00000000..b25b0625 --- /dev/null +++ b/dashboard/horizon/site_urls.py @@ -0,0 +1,45 @@ +# 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 django.views.generic import TemplateView +from django.conf.urls.defaults import patterns, url, include +from django.conf import settings + + +urlpatterns = patterns('horizon.views', + url(r'home/$', 'user_home', name='user_home') +) + +# Client-side i18n URLconf. +urlpatterns += patterns('', + url(r'^i18n/js/(?P\S+?)/$', + 'django.views.i18n.javascript_catalog', + name='jsi18n'), + url(r'^i18n/setlang/$', + 'django.views.i18n.set_language', + name="set_language"), + url(r'^i18n/', include('django.conf.urls.i18n')) +) + +if settings.DEBUG: + urlpatterns += patterns('', + url(r'^qunit/$', + TemplateView.as_view(template_name="horizon/qunit.html"), + name='qunit_tests')) diff --git a/dashboard/horizon/static/bootstrap/js/bootstrap.js b/dashboard/horizon/static/bootstrap/js/bootstrap.js new file mode 100644 index 00000000..4412304e --- /dev/null +++ b/dashboard/horizon/static/bootstrap/js/bootstrap.js @@ -0,0 +1,1720 @@ +/* =================================================== + * bootstrap-transition.js v2.0.1 + * http://twitter.github.com/bootstrap/javascript.html#transitions + * =================================================== + * Copyright 2012 Twitter, 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. + * ========================================================== */ + +!function( $ ) { + + $(function () { + + "use strict" + + /* CSS TRANSITION SUPPORT (https://gist.github.com/373874) + * ======================================================= */ + + $.support.transition = (function () { + var thisBody = document.body || document.documentElement + , thisStyle = thisBody.style + , support = thisStyle.transition !== undefined || thisStyle.WebkitTransition !== undefined || thisStyle.MozTransition !== undefined || thisStyle.MsTransition !== undefined || thisStyle.OTransition !== undefined + + return support && { + end: (function () { + var transitionEnd = "TransitionEnd" + if ( $.browser.webkit ) { + transitionEnd = "webkitTransitionEnd" + } else if ( $.browser.mozilla ) { + transitionEnd = "transitionend" + } else if ( $.browser.opera ) { + transitionEnd = "oTransitionEnd" + } + return transitionEnd + }()) + } + })() + + }) + +}( window.jQuery );/* ========================================================== + * bootstrap-alert.js v2.0.1 + * http://twitter.github.com/bootstrap/javascript.html#alerts + * ========================================================== + * Copyright 2012 Twitter, 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. + * ========================================================== */ + + +!function( $ ){ + + "use strict" + + /* ALERT CLASS DEFINITION + * ====================== */ + + var dismiss = '[data-dismiss="alert"]' + , Alert = function ( el ) { + $(el).on('click', dismiss, this.close) + } + + Alert.prototype = { + + constructor: Alert + + , close: function ( e ) { + var $this = $(this) + , selector = $this.attr('data-target') + , $parent + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = $(selector) + $parent.trigger('close') + + e && e.preventDefault() + + $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) + + $parent + .trigger('close') + .removeClass('in') + + function removeElement() { + $parent + .trigger('closed') + .remove() + } + + $.support.transition && $parent.hasClass('fade') ? + $parent.on($.support.transition.end, removeElement) : + removeElement() + } + + } + + + /* ALERT PLUGIN DEFINITION + * ======================= */ + + $.fn.alert = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('alert') + if (!data) $this.data('alert', (data = new Alert(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.alert.Constructor = Alert + + + /* ALERT DATA-API + * ============== */ + + $(function () { + $('body').on('click.alert.data-api', dismiss, Alert.prototype.close) + }) + +}( window.jQuery );/* ============================================================ + * bootstrap-button.js v2.0.1 + * http://twitter.github.com/bootstrap/javascript.html#buttons + * ============================================================ + * Copyright 2012 Twitter, 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. + * ============================================================ */ + +!function( $ ){ + + "use strict" + + /* BUTTON PUBLIC CLASS DEFINITION + * ============================== */ + + var Button = function ( element, options ) { + this.$element = $(element) + this.options = $.extend({}, $.fn.button.defaults, options) + } + + Button.prototype = { + + constructor: Button + + , setState: function ( state ) { + var d = 'disabled' + , $el = this.$element + , data = $el.data() + , val = $el.is('input') ? 'val' : 'html' + + state = state + 'Text' + data.resetText || $el.data('resetText', $el[val]()) + + $el[val](data[state] || this.options[state]) + + // push to event loop to allow forms to submit + setTimeout(function () { + state == 'loadingText' ? + $el.addClass(d).attr(d, d) : + $el.removeClass(d).removeAttr(d) + }, 0) + } + + , toggle: function () { + var $parent = this.$element.parent('[data-toggle="buttons-radio"]') + + $parent && $parent + .find('.active') + .removeClass('active') + + this.$element.toggleClass('active') + } + + } + + + /* BUTTON PLUGIN DEFINITION + * ======================== */ + + $.fn.button = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('button') + , options = typeof option == 'object' && option + if (!data) $this.data('button', (data = new Button(this, options))) + if (option == 'toggle') data.toggle() + else if (option) data.setState(option) + }) + } + + $.fn.button.defaults = { + loadingText: 'loading...' + } + + $.fn.button.Constructor = Button + + + /* BUTTON DATA-API + * =============== */ + + $(function () { + $('body').on('click.button.data-api', '[data-toggle^=button]', function ( e ) { + var $btn = $(e.target) + if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') + $btn.button('toggle') + }) + }) + +}( window.jQuery );/* ========================================================== + * bootstrap-carousel.js v2.0.1 + * http://twitter.github.com/bootstrap/javascript.html#carousel + * ========================================================== + * Copyright 2012 Twitter, 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. + * ========================================================== */ + + +!function( $ ){ + + "use strict" + + /* CAROUSEL CLASS DEFINITION + * ========================= */ + + var Carousel = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.carousel.defaults, options) + this.options.slide && this.slide(this.options.slide) + } + + Carousel.prototype = { + + cycle: function () { + this.interval = setInterval($.proxy(this.next, this), this.options.interval) + return this + } + + , to: function (pos) { + var $active = this.$element.find('.active') + , children = $active.parent().children() + , activePos = children.index($active) + , that = this + + if (pos > (children.length - 1) || pos < 0) return + + if (this.sliding) { + return this.$element.one('slid', function () { + that.to(pos) + }) + } + + if (activePos == pos) { + return this.pause().cycle() + } + + return this.slide(pos > activePos ? 'next' : 'prev', $(children[pos])) + } + + , pause: function () { + clearInterval(this.interval) + this.interval = null + return this + } + + , next: function () { + if (this.sliding) return + return this.slide('next') + } + + , prev: function () { + if (this.sliding) return + return this.slide('prev') + } + + , slide: function (type, next) { + var $active = this.$element.find('.active') + , $next = next || $active[type]() + , isCycling = this.interval + , direction = type == 'next' ? 'left' : 'right' + , fallback = type == 'next' ? 'first' : 'last' + , that = this + + if (!$next.length) return + + this.sliding = true + + isCycling && this.pause() + + $next = $next.length ? $next : this.$element.find('.item')[fallback]() + + if (!$.support.transition && this.$element.hasClass('slide')) { + this.$element.trigger('slide') + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger('slid') + } else { + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + this.$element.trigger('slide') + this.$element.one($.support.transition.end, function () { + $next.removeClass([type, direction].join(' ')).addClass('active') + $active.removeClass(['active', direction].join(' ')) + that.sliding = false + setTimeout(function () { that.$element.trigger('slid') }, 0) + }) + } + + isCycling && this.cycle() + + return this + } + + } + + + /* CAROUSEL PLUGIN DEFINITION + * ========================== */ + + $.fn.carousel = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('carousel') + , options = typeof option == 'object' && option + if (!data) $this.data('carousel', (data = new Carousel(this, options))) + if (typeof option == 'number') data.to(option) + else if (typeof option == 'string' || (option = options.slide)) data[option]() + else data.cycle() + }) + } + + $.fn.carousel.defaults = { + interval: 5000 + } + + $.fn.carousel.Constructor = Carousel + + + /* CAROUSEL DATA-API + * ================= */ + + $(function () { + $('body').on('click.carousel.data-api', '[data-slide]', function ( e ) { + var $this = $(this), href + , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + , options = !$target.data('modal') && $.extend({}, $target.data(), $this.data()) + $target.carousel(options) + e.preventDefault() + }) + }) + +}( window.jQuery );/* ============================================================= + * bootstrap-collapse.js v2.0.1 + * http://twitter.github.com/bootstrap/javascript.html#collapse + * ============================================================= + * Copyright 2012 Twitter, 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. + * ============================================================ */ + +!function( $ ){ + + "use strict" + + var Collapse = function ( element, options ) { + this.$element = $(element) + this.options = $.extend({}, $.fn.collapse.defaults, options) + + if (this.options["parent"]) { + this.$parent = $(this.options["parent"]) + } + + this.options.toggle && this.toggle() + } + + Collapse.prototype = { + + constructor: Collapse + + , dimension: function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } + + , show: function () { + var dimension = this.dimension() + , scroll = $.camelCase(['scroll', dimension].join('-')) + , actives = this.$parent && this.$parent.find('.in') + , hasData + + if (actives && actives.length) { + hasData = actives.data('collapse') + actives.collapse('hide') + hasData || actives.data('collapse', null) + } + + this.$element[dimension](0) + this.transition('addClass', 'show', 'shown') + this.$element[dimension](this.$element[0][scroll]) + + } + + , hide: function () { + var dimension = this.dimension() + this.reset(this.$element[dimension]()) + this.transition('removeClass', 'hide', 'hidden') + this.$element[dimension](0) + } + + , reset: function ( size ) { + var dimension = this.dimension() + + this.$element + .removeClass('collapse') + [dimension](size || 'auto') + [0].offsetWidth + + this.$element.addClass('collapse') + } + + , transition: function ( method, startEvent, completeEvent ) { + var that = this + , complete = function () { + if (startEvent == 'show') that.reset() + that.$element.trigger(completeEvent) + } + + this.$element + .trigger(startEvent) + [method]('in') + + $.support.transition && this.$element.hasClass('collapse') ? + this.$element.one($.support.transition.end, complete) : + complete() + } + + , toggle: function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() + } + + } + + /* COLLAPSIBLE PLUGIN DEFINITION + * ============================== */ + + $.fn.collapse = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('collapse') + , options = typeof option == 'object' && option + if (!data) $this.data('collapse', (data = new Collapse(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.collapse.defaults = { + toggle: true + } + + $.fn.collapse.Constructor = Collapse + + + /* COLLAPSIBLE DATA-API + * ==================== */ + + $(function () { + $('body').on('click.collapse.data-api', '[data-toggle=collapse]', function ( e ) { + var $this = $(this), href + , target = $this.attr('data-target') + || e.preventDefault() + || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 + , option = $(target).data('collapse') ? 'toggle' : $this.data() + $(target).collapse(option) + }) + }) + +}( window.jQuery );/* ============================================================ + * bootstrap-dropdown.js v2.0.1 + * http://twitter.github.com/bootstrap/javascript.html#dropdowns + * ============================================================ + * Copyright 2012 Twitter, 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. + * ============================================================ */ + + +!function( $ ){ + + "use strict" + + /* DROPDOWN CLASS DEFINITION + * ========================= */ + + var toggle = '[data-toggle="dropdown"]' + , Dropdown = function ( element ) { + var $el = $(element).on('click.dropdown.data-api', this.toggle) + $('html').on('click.dropdown.data-api', function () { + $el.parent().removeClass('open') + }) + } + + Dropdown.prototype = { + + constructor: Dropdown + + , toggle: function ( e ) { + var $this = $(this) + , selector = $this.attr('data-target') + , $parent + , isActive + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = $(selector) + $parent.length || ($parent = $this.parent()) + + isActive = $parent.hasClass('open') + + clearMenus() + !isActive && $parent.toggleClass('open') + + return false + } + + } + + function clearMenus() { + $(toggle).parent().removeClass('open') + } + + + /* DROPDOWN PLUGIN DEFINITION + * ========================== */ + + $.fn.dropdown = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('dropdown') + if (!data) $this.data('dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.dropdown.Constructor = Dropdown + + + /* APPLY TO STANDARD DROPDOWN ELEMENTS + * =================================== */ + + $(function () { + $('html').on('click.dropdown.data-api', clearMenus) + $('body').on('click.dropdown.data-api', toggle, Dropdown.prototype.toggle) + }) + +}( window.jQuery );/* ========================================================= + * bootstrap-modal.js v2.0.1 + * http://twitter.github.com/bootstrap/javascript.html#modals + * ========================================================= + * Copyright 2012 Twitter, 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. + * ========================================================= */ + + +!function( $ ){ + + "use strict" + + /* MODAL CLASS DEFINITION + * ====================== */ + + var Modal = function ( content, options ) { + this.options = options + this.$element = $(content) + .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) + } + + Modal.prototype = { + + constructor: Modal + + , toggle: function () { + return this[!this.isShown ? 'show' : 'hide']() + } + + , show: function () { + var that = this + + if (this.isShown) return + + $('body').addClass('modal-open') + + this.isShown = true + this.$element.trigger('show') + + escape.call(this) + backdrop.call(this, function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + !that.$element.parent().length && that.$element.appendTo(document.body) //don't move modals dom position + + that.$element + .show() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element.addClass('in') + + transition ? + that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) : + that.$element.trigger('shown') + + }) + } + + , hide: function ( e ) { + e && e.preventDefault() + + if (!this.isShown) return + + var that = this + this.isShown = false + + $('body').removeClass('modal-open') + + escape.call(this) + + this.$element + .trigger('hide') + .removeClass('in') + + $.support.transition && this.$element.hasClass('fade') ? + hideWithTransition.call(this) : + hideModal.call(this) + } + + } + + + /* MODAL PRIVATE METHODS + * ===================== */ + + function hideWithTransition() { + var that = this + , timeout = setTimeout(function () { + that.$element.off($.support.transition.end) + hideModal.call(that) + }, 500) + + this.$element.one($.support.transition.end, function () { + clearTimeout(timeout) + hideModal.call(that) + }) + } + + function hideModal( that ) { + this.$element + .hide() + .trigger('hidden') + + backdrop.call(this) + } + + function backdrop( callback ) { + var that = this + , animate = this.$element.hasClass('fade') ? 'fade' : '' + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $('