├── overmind ├── __init__.py ├── api │ ├── __init__.py │ ├── models.py │ ├── tests.py │ ├── test_urls.py │ ├── urls.py │ ├── provisioning.py │ └── test_provisioning.py ├── provisioning │ ├── __init__.py │ ├── tests.py │ ├── management │ │ ├── commands │ │ │ ├── __init__.py │ │ │ └── create_groups.py │ │ └── __init__.py │ ├── plugins │ │ ├── __init__.py │ │ ├── dedicated.py │ │ └── hetzner.py │ ├── fixtures │ │ └── initial_data.json │ ├── provider_meta.py │ ├── tasks.py │ ├── controllers.py │ ├── forms.py │ ├── views.py │ └── models.py ├── media │ ├── images │ │ ├── ajax-loader.gif │ │ ├── sidebar_background.png │ │ └── sidebar_section_background.png │ ├── css │ │ ├── jquery.tooltip.css │ │ └── default.css │ └── js │ │ ├── main.js │ │ ├── jquery.tooltip.min.js │ │ └── jquery.dimensions.js ├── templates │ ├── provider_form.html │ ├── image_form.html │ ├── registration │ │ ├── register.html │ │ └── login.html │ ├── node_form.html │ ├── provider.html │ ├── base.html │ ├── overview.html │ ├── settings.html │ └── node.html ├── test_urls.py ├── manage.py ├── urls.py └── settings.py ├── .gitignore ├── AUTHORS ├── requirements.txt ├── CHANGELOG ├── LICENSE ├── README.md └── COPYING /overmind/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /overmind/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /overmind/provisioning/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /overmind/provisioning/tests.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /overmind/provisioning/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | lib 3 | include 4 | distribute*.tar.gz 5 | *.pyc 6 | 7 | -------------------------------------------------------------------------------- /overmind/api/models.py: -------------------------------------------------------------------------------- 1 | #The api app needs an empty models.py so that it gets registered for unit test detection 2 | -------------------------------------------------------------------------------- /overmind/media/images/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobami/overmind/HEAD/overmind/media/images/ajax-loader.gif -------------------------------------------------------------------------------- /overmind/media/images/sidebar_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobami/overmind/HEAD/overmind/media/images/sidebar_background.png -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | The PRIMARY AUTHORS are: 2 | 3 | Miquel Torres 4 | Grig Gheorghiu 5 | 6 | 7 | CONTRIBUTORS: 8 | 9 | 10 | THANKS: 11 | 12 | -------------------------------------------------------------------------------- /overmind/media/images/sidebar_section_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobami/overmind/HEAD/overmind/media/images/sidebar_section_background.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.3.5 2 | django-piston==0.2.3 3 | celery==2.5.5 4 | django-celery==2.5.5 5 | IPy==0.81 6 | apache-libcloud==0.12.4 7 | httplib2==0.9 8 | -------------------------------------------------------------------------------- /overmind/templates/provider_form.html: -------------------------------------------------------------------------------- 1 | {{ form.as_p }} 2 |
3 | {% if error %}Error: {{ error }}
{% endif %} -------------------------------------------------------------------------------- /overmind/api/tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import test_provisioning 3 | 4 | 5 | def suite(): 6 | provisioning = test_provisioning.suite() 7 | return unittest.TestSuite([provisioning]) 8 | -------------------------------------------------------------------------------- /overmind/templates/image_form.html: -------------------------------------------------------------------------------- 1 | {{ form.as_p }} 2 | 3 | {% if error %}Error: {{ error }}
{% endif %} 4 | -------------------------------------------------------------------------------- /overmind/test_urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, include 2 | from urls import urlpatterns as normal_urlpatterns 3 | 4 | urlpatterns = patterns('', 5 | (r'^api/', include('overmind.api.test_urls')) 6 | ) 7 | 8 | urlpatterns += normal_urlpatterns 9 | -------------------------------------------------------------------------------- /overmind/media/css/jquery.tooltip.css: -------------------------------------------------------------------------------- 1 | #tooltip { 2 | position: absolute; 3 | z-index: 3000; 4 | border: 1px solid #111; 5 | background-color: #eee; 6 | padding: 5px; 7 | opacity: 0.85; 8 | max-width: 29.4em; 9 | } 10 | #tooltip h3, #tooltip div { margin: 0; } 11 | -------------------------------------------------------------------------------- /overmind/templates/registration/register.html: -------------------------------------------------------------------------------- 1 | {% block content %} 2 | {{ form.as_p }} 3 | 4 | {% if editing and perms.auth %}delete{% endif %} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | =================== 2 | Overmind Change Log 3 | =================== 4 | 5 | Next Version 6 | ------------ 7 | 8 | * Renaming of realm=>location and flavor=>sizes 9 | * Saving of Images, Locations and Sizes 10 | 11 | 12 | Version 0.1.0, October 14, 2010 13 | -------------------------------- 14 | 15 | * EC2 and Rackspace server provisioning 16 | * Provider Plugins: “Dedicated Hardware” and "Hetzner" 17 | * REST API for provider and nodes 18 | * Authentication with three user roles 19 | -------------------------------------------------------------------------------- /overmind/provisioning/management/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Creates predefined user Roles 3 | """ 4 | from django.db.models import signals 5 | from django.core.management import call_command 6 | from provisioning import models as provisioning_app 7 | 8 | def create_groups(app, created_models, verbosity, **kwargs): 9 | call_command("create_groups") 10 | 11 | signals.post_syncdb.connect( 12 | create_groups, 13 | sender=provisioning_app, 14 | dispatch_uid ="provisioning.management.create_groups" 15 | ) 16 | -------------------------------------------------------------------------------- /overmind/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | try: 4 | import settings # Assumed to be in the same directory. 5 | except ImportError: 6 | import sys 7 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 8 | sys.exit(1) 9 | 10 | if __name__ == "__main__": 11 | execute_manager(settings) 12 | -------------------------------------------------------------------------------- /overmind/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} Login{% endblock %} 4 | 5 | {% block content %} 6 | 7 | {% if form.errors %} 8 |Invalid username or password
9 | {% endif %} 10 | 11 | 20 | 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /overmind/api/test_urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from api.provisioning import ProviderHandler, NodeHandler, ImageHandler 3 | import api 4 | from urls import CsrfExemptResource 5 | 6 | 7 | # The test url creates resources that do not require authentication 8 | api.provisioning._TESTING = True 9 | 10 | provider_resource = CsrfExemptResource(ProviderHandler) 11 | image_resource = CsrfExemptResource(ImageHandler) 12 | node_resource = CsrfExemptResource(NodeHandler) 13 | 14 | urlpatterns = patterns('', 15 | url(r'^providers/(?PError: {{ error }}
{% endif %} -------------------------------------------------------------------------------- /overmind/provisioning/fixtures/initial_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "provisioning.action", 4 | "pk": 1, 5 | "fields": { 6 | "name": "images", 7 | "show": false 8 | } 9 | }, 10 | { 11 | "model": "provisioning.action", 12 | "pk": 2, 13 | "fields": { 14 | "name": "sizes", 15 | "show": false 16 | } 17 | }, 18 | { 19 | "model": "provisioning.action", 20 | "pk": 3, 21 | "fields": { 22 | "name": "locations", 23 | "show": false 24 | } 25 | }, 26 | { 27 | "model": "provisioning.action", 28 | "pk": 4, 29 | "fields": { 30 | "name": "create", 31 | "show": false 32 | } 33 | }, 34 | { 35 | "model": "provisioning.action", 36 | "pk": 5, 37 | "fields": { 38 | "name": "destroy", 39 | "show": true 40 | } 41 | }, 42 | { 43 | "model": "provisioning.action", 44 | "pk": 6, 45 | "fields": { 46 | "name": "reboot", 47 | "show": true 48 | } 49 | }, 50 | { 51 | "model": "provisioning.action", 52 | "pk": 7, 53 | "fields": { 54 | "name": "list", 55 | "show": false 56 | } 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /overmind/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from piston.resource import Resource 3 | from piston.authentication import HttpBasicAuthentication 4 | 5 | from api.provisioning import ProviderHandler, NodeHandler, ImageHandler 6 | 7 | 8 | auth = HttpBasicAuthentication(realm="overmind") 9 | ad = { 'authentication': auth } 10 | 11 | class CsrfExemptResource(Resource): 12 | '''Django 1.2 CSRF protection can interfere''' 13 | def __init__(self, handler, authentication = None): 14 | super(CsrfExemptResource, self).__init__(handler, authentication) 15 | self.csrf_exempt = getattr(self.handler, 'csrf_exempt', True) 16 | 17 | provider_resource = CsrfExemptResource(ProviderHandler, **ad) 18 | image_resource = CsrfExemptResource(ImageHandler, **ad) 19 | node_resource = CsrfExemptResource(NodeHandler, **ad) 20 | 21 | urlpatterns = patterns('', 22 | url(r'^providers/(?P{{ user.username }} | Logout
{% endif %} 30 || Name | first_name | last_name | Role | |
|---|---|---|---|---|
| {{ u.username }} | {{ u.first_name }} | {{ u.last_name }} | {{ u.email }} | {{ u.groups.all.0 }} | 52 |
You don't have permissions to edit or create other users
60 | 61 | 62 | {% endif %} 63 | {% else %}You don't have permissions to access this page
64 | {% endif %} 65 | {% endblock %} 66 | 67 | {% block sidebar %} 68 | 75 | {% endblock %} 76 | -------------------------------------------------------------------------------- /overmind/media/js/main.js: -------------------------------------------------------------------------------- 1 | $(document).ajaxSend(function(event, xhr, settings) { 2 | function getCookie(name) { 3 | var cookieValue = null; 4 | if (document.cookie && document.cookie != '') { 5 | var cookies = document.cookie.split(';'); 6 | for (var i = 0; i < cookies.length; i++) { 7 | var cookie = jQuery.trim(cookies[i]); 8 | // Does this cookie string begin with the name we want? 9 | if (cookie.substring(0, name.length + 1) == (name + '=')) { 10 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); 11 | break; 12 | } 13 | } 14 | } 15 | return cookieValue; 16 | } 17 | function sameOrigin(url) { 18 | // url could be relative or scheme relative or absolute 19 | var host = document.location.host; // host + port 20 | var protocol = document.location.protocol; 21 | var sr_origin = '//' + host; 22 | var origin = protocol + sr_origin; 23 | // Allow absolute or scheme relative URLs to same origin 24 | return (url == origin || url.slice(0, origin.length + 1) == origin + '/') || 25 | (url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') || 26 | // or any other URL that isn't scheme relative or absolute i.e relative. 27 | !(/^(\/\/|http:|https:).*/.test(url)); 28 | } 29 | function safeMethod(method) { 30 | return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); 31 | } 32 | 33 | if (!safeMethod(settings.type) && sameOrigin(settings.url)) { 34 | xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken')); 35 | } 36 | }); 37 | 38 | 39 | function confirmation(message, target) { 40 | var answer = confirm(message); 41 | if (answer){ 42 | window.location = target; 43 | } 44 | } 45 | 46 | function ajaxSubmit(form, redirect) { 47 | var toOpacity = 0.3; 48 | var duration = 100; 49 | //check the form is not currently submitting 50 | if(form.data('formstatus') !== 'submitting'){ 51 | //setup variables 52 | formData = form.serialize(); 53 | formUrl = form.attr('action'); 54 | formMethod = form.attr('method'); 55 | 56 | //add status data to form 57 | form.data('formstatus','submitting'); 58 | 59 | //transition 60 | form.fadeTo(duration, toOpacity); 61 | $('#loading').show(); 62 | 63 | //send data to server for validation 64 | $.ajax({ 65 | url: formUrl, 66 | type: formMethod, 67 | data: formData, 68 | success:function(data){ 69 | $('#loading').hide(); 70 | if (data === "success
") { 71 | if (redirect) { window.location = redirect; } 72 | } 73 | form.data('formstatus','ready'); 74 | form.html(data); 75 | form.fadeTo(duration,1); 76 | } 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /overmind/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for Overmind project. 2 | import os, logging 3 | import djcelery 4 | 5 | djcelery.setup_loader() 6 | BROKER_HOST = "localhost" 7 | BROKER_PORT = 5672 8 | BROKER_USER = "guest" 9 | BROKER_PASSWORD = "guest" 10 | BROKER_VHOST = "/" 11 | 12 | DEBUG = True 13 | TEMPLATE_DEBUG = DEBUG 14 | 15 | BASEDIR = os.path.abspath( os.path.dirname(__file__).replace('\\','/') ) 16 | 17 | ADMINS = ( 18 | # ('Your Name', 'your_email@domain.com'), 19 | ) 20 | 21 | MANAGERS = ADMINS 22 | 23 | DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 24 | DATABASE_NAME = 'data.db' # Or path to database file if using sqlite3. 25 | DATABASE_USER = '' # Not used with sqlite3. 26 | DATABASE_PASSWORD = '' # Not used with sqlite3. 27 | DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. 28 | DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. 29 | 30 | # Local time zone for this installation. Choices can be found here: 31 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 32 | # although not all choices may be available on all operating systems. 33 | # If running in a Windows environment this must be set to the same as your 34 | # system time zone. 35 | TIME_ZONE = 'America/Chicago' 36 | 37 | # Language code for this installation. All choices can be found here: 38 | # http://www.i18nguy.com/unicode/language-identifiers.html 39 | LANGUAGE_CODE = 'en-us' 40 | 41 | SITE_ID = 1 42 | 43 | # If you set this to False, Django will make some optimizations so as not 44 | # to load the internationalization machinery. 45 | USE_I18N = True 46 | 47 | # Absolute path to the directory that holds media. 48 | # Example: "/home/media/media.lawrence.com/" 49 | MEDIA_ROOT = BASEDIR + '/media/' 50 | 51 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 52 | # trailing slash if there is a path component (optional in other cases). 53 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 54 | MEDIA_URL = '/media/' 55 | 56 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 57 | # trailing slash. 58 | # Examples: "http://foo.com/media/", "/media/". 59 | ADMIN_MEDIA_PREFIX = '/admin_media/' 60 | 61 | # Make this unique, and don't share it with anybody. 62 | SECRET_KEY = 'po7j6y(so=k75zpzu4^fpquj%&^s9j$ix9se9kth(9qi!0(z&s' 63 | 64 | # List of callables that know how to import templates from various sources. 65 | TEMPLATE_LOADERS = ( 66 | 'django.template.loaders.filesystem.load_template_source', 67 | 'django.template.loaders.app_directories.load_template_source', 68 | # 'django.template.loaders.eggs.load_template_source', 69 | ) 70 | 71 | MIDDLEWARE_CLASSES = ( 72 | 'django.middleware.common.CommonMiddleware', 73 | 'django.contrib.sessions.middleware.SessionMiddleware', 74 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 75 | 'django.middleware.csrf.CsrfViewMiddleware', 76 | 'django.middleware.csrf.CsrfResponseMiddleware', 77 | ) 78 | 79 | ROOT_URLCONF = 'overmind.urls' 80 | 81 | TEMPLATE_DIRS = ( 82 | os.path.join(os.path.dirname(__file__), 'templates').replace('\\','/'), 83 | ) 84 | 85 | INSTALLED_APPS = ( 86 | 'django.contrib.auth', 87 | 'django.contrib.contenttypes', 88 | 'django.contrib.sessions', 89 | 'djcelery', 90 | 'django.contrib.sites', 91 | 'provisioning', 92 | 'api', 93 | ) 94 | 95 | PUBLIC_KEY_FILE = "id_rsa.pub" 96 | PUBLIC_KEY = open(os.path.expanduser("~/.ssh/%s" % PUBLIC_KEY_FILE)).read() 97 | 98 | # Configure logging 99 | if DEBUG: 100 | logging.basicConfig( 101 | level = logging.DEBUG, 102 | format = '%(asctime)s %(levelname)s: %(message)s', 103 | ) 104 | else: 105 | logging.basicConfig( 106 | level = logging.INFO, 107 | format = '%(asctime)s %(levelname)s: %(message)s', 108 | filename = BASEDIR + '/log.log', 109 | filemode = 'w', 110 | ) 111 | -------------------------------------------------------------------------------- /overmind/provisioning/plugins/hetzner.py: -------------------------------------------------------------------------------- 1 | # Hetzner plugin 2 | import json 3 | from urllib import urlencode 4 | 5 | from libcloud.compute.base import NodeDriver, Node 6 | from libcloud.compute.types import NodeState, InvalidCredsException 7 | import httplib2 8 | 9 | display_name = "Hetzner" 10 | access_key = 'User' 11 | secret_key = 'Password' 12 | form_fields = None 13 | # It seems that reboot (reset in the Hetzner API) doesn't work, so don't add 14 | supported_actions = ['list'] 15 | 16 | 17 | class Connection(): 18 | host = "https://robot-ws.your-server.de/" 19 | 20 | def __init__(self, user, password): 21 | self.conn = httplib2.Http(".cache") 22 | self.conn.add_credentials(user, password) 23 | 24 | def _raise_error(self, response, content): 25 | if response.get('status') == '400': 26 | raise Exception, "Invalid parameters" 27 | elif response.get('status') == '401': 28 | raise InvalidCredsException 29 | elif response.get('status') == '404' and content == 'Server not found': 30 | raise Exception, "Server not found" 31 | elif response.get('status') == '404': 32 | raise Exception, "Reset not available" 33 | elif response.get('status') == '500' and content == 'Reset failed': 34 | raise Exception, "Reset failed" 35 | else: 36 | raise Exception, "Unknown error: " + response.get('status') 37 | 38 | def request(self, path, method='GET', params=None): 39 | if method != 'GET' and method != 'POST': return None 40 | data = None 41 | if params: data = urlencode(params) 42 | response, content = self.conn.request( 43 | self.host + path, 44 | method, 45 | data, 46 | ) 47 | if response.get('status') == '200': 48 | return json.loads(content) 49 | else: 50 | self._raise_error(response, content) 51 | 52 | 53 | class Driver(NodeDriver): 54 | name = display_name 55 | type = 0 56 | 57 | NODE_STATE_MAP = { 58 | 'ready': NodeState.RUNNING, 59 | 'process': NodeState.PENDING, 60 | } 61 | 62 | def __init__(self, user, password): 63 | self.connection = Connection(user, password) 64 | 65 | def _parse_nodes(self, data): 66 | nodes = [] 67 | for n in data: 68 | nodedata = n['server'] 69 | response = self.connection.request('server/%s' % nodedata['server_ip']) 70 | nodedata['extra_ips'] = ", ".join(response['server']['ip']) 71 | # dict.get() will return None even if we write get('subnet', []) 72 | subnets = response['server'].get('subnet') or [] 73 | nodedata['subnet'] = ", ".join(s['ip'] for s in subnets) 74 | nodes.append(nodedata) 75 | return nodes 76 | 77 | def _to_node(self, el): 78 | public_ip = [el.get('server_ip')] 79 | n = Node(id=el.get('server_ip').replace(".",""), 80 | name=el.get('server_ip'), 81 | state=self.NODE_STATE_MAP.get(el.get('status'), NodeState.UNKNOWN), 82 | public_ip=public_ip, 83 | private_ip=[], 84 | driver=self, 85 | extra={ 86 | 'location': el.get('dc'), 87 | 'product': el.get('product'), 88 | 'traffic': el.get('traffic'), 89 | 'paid_until': el.get('paid_until'), 90 | 'extra_ips': el.get('extra_ips'), 91 | 'subnet': el.get('subnet'), 92 | }) 93 | return n 94 | 95 | def list_nodes(self): 96 | #TODO: 404 error "No server found" needs to be handled 97 | response = self.connection.request('server') 98 | nodes = [] 99 | for node in self._parse_nodes(response): 100 | nodes.append(self._to_node(node)) 101 | return nodes 102 | 103 | def reboot(self, node): 104 | params = { 'type': 'sw' }#Support hd reset? 105 | response = self.connection.request( 106 | 'reset/' + node.public_ip[0] + "/", method='POST', params=params 107 | ) 108 | -------------------------------------------------------------------------------- /overmind/templates/node.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}: New Node{% endblock %} 3 | {% block script %} 4 | 92 | {% endblock %} 93 | {% block sidebar %}{% endblock %} 94 | 95 | {% block content %} 96 |No providers configured yet{% endif %} 101 |
| " + field[0] + ": | " + str(field[1]) 47 | datatable += " |
success
') 105 | else: 106 | form = ProviderForm(request.GET.get("provider_type")) 107 | if error == 'form': 108 | error = None 109 | return render_to_response('provider_form.html', 110 | { 'form': form, 'error': error }) 111 | 112 | def save_new_provider(data): 113 | form = ProviderForm(data.get('provider_type'), data) 114 | return save_provider(form) 115 | 116 | def update_provider(data, provider): 117 | form = ProviderForm(data.get('provider_type'), data, instance=provider) 118 | return save_provider(form) 119 | 120 | def save_provider(form): 121 | error = None 122 | if form.is_valid(): 123 | provider = None 124 | try: 125 | provider = form.save() 126 | # Make sure the credentials are correct 127 | provider.check_credentials() 128 | 129 | logging.info('Provider saved %s' % provider.name) 130 | result = tasks.import_provider_info.delay(provider.id) 131 | except InvalidCredsException: 132 | # Delete provider if InvalidCreds is raised (by EC2) 133 | # after it has been saved 134 | if provider: 135 | provider.delete() 136 | # Return form with InvalidCreds error 137 | error = 'Invalid account credentials' 138 | else: 139 | return None, form, provider 140 | else: 141 | error = 'form' 142 | return error, form, None 143 | 144 | @login_required 145 | def updateproviders(request): 146 | providers = Provider.objects.all() 147 | for provider in providers: 148 | if provider.supports('list'): 149 | provider.update() 150 | return HttpResponseRedirect('/overview/') 151 | 152 | @permission_required('provisioning.delete_provider') 153 | def deleteprovider(request, provider_id): 154 | provider = Provider.objects.get(id=provider_id) 155 | provider.delete() 156 | return HttpResponseRedirect('/overview/') 157 | 158 | @permission_required('provisioning.add_node') 159 | def node(request): 160 | '''Displays a provider selection list to call the node creation form''' 161 | variables = RequestContext(request, { 162 | 'provider_list': Action.objects.get(name='create').provider_set.all(), 163 | }) 164 | return render_to_response('node.html', variables) 165 | 166 | @permission_required('provisioning.add_node') 167 | def addimage(request): 168 | error = None 169 | if request.method == 'POST': 170 | form = AddImageForm(request.POST.get("provider"), request.POST) 171 | if form.is_valid(): 172 | img = form.cleaned_data['image'] 173 | img.favorite = True 174 | img.save() 175 | favimage = {'name': img.name, 'image_id': img.image_id, 'id': img.id} 176 | return HttpResponse(json.dumps(favimage)) 177 | else: 178 | form = AddImageForm(request.GET.get("provider")) 179 | return render_to_response('image_form.html', { 'form': form, 'error': error }) 180 | 181 | @permission_required('provisioning.add_node') 182 | def removeimage(request, image_id): 183 | if request.method == 'POST': 184 | try: 185 | image = Image.objects.get(id=image_id) 186 | image.favorite = False 187 | image.save() 188 | return HttpResponse("SUCCESS
" % image) 189 | except Image.DoesNotExist: 190 | error = "Image id %s does not exist
" % image_id 191 | else: 192 | error = "Only POST Allowed
" 193 | return HttpResponse(error) 194 | 195 | @permission_required('provisioning.add_node') 196 | def newnode(request): 197 | error = None 198 | favcount = 0 199 | if request.method == 'POST': 200 | error, form, node = save_new_node(request.POST, request.user) 201 | if error is None: 202 | return HttpResponse('success
') 203 | else: 204 | form = NodeForm(request.GET.get("provider")) 205 | favcount = Image.objects.filter( 206 | provider=request.GET.get("provider"), 207 | favorite=True 208 | ).count() 209 | if error == 'form': 210 | error = None 211 | return render_to_response('node_form.html', 212 | { 'form': form, 'favcount': favcount, 'error': error }) 213 | 214 | def save_new_node(data, user): 215 | provider_id = data.get("provider") 216 | if not provider_id: 217 | return 'Incorrect provider id', None, None 218 | error = None 219 | form = None 220 | try: 221 | provider = Provider.objects.get(id=provider_id) 222 | form = NodeForm(provider_id, data) 223 | except Provider.DoesNotExist: 224 | error = 'Incorrect provider id' 225 | 226 | if form is not None: 227 | if form.is_valid(): 228 | try: 229 | node = Node.objects.get( 230 | provider=provider, name=form.cleaned_data['name'] 231 | ) 232 | error = 'A node with that name already exists' 233 | except Node.DoesNotExist: 234 | error, data_from_provider = provider.create_node(form) 235 | if error is None: 236 | node = form.save(commit = False) 237 | node.node_id = str(data_from_provider['node_id']) 238 | node.public_ip = data_from_provider['public_ip'] 239 | node.private_ip = data_from_provider.get('private_ip', '') 240 | node.state = get_state(data_from_provider['state']) 241 | node.created_by = user.username 242 | node.save_extra_data(data_from_provider.get('extra', '')) 243 | try: 244 | node.save() 245 | logging.info('New node created %s' % node) 246 | # Mark image as recently used by saving it 247 | if node.image is not None: 248 | node.image.save() 249 | return None, form, node 250 | except Exception, e: 251 | error = e 252 | logging.error('Could not create node: %s' % e) 253 | else: 254 | error = 'form' 255 | return error, form, None 256 | 257 | @permission_required('provisioning.change_node') 258 | def rebootnode(request, node_id): 259 | node = Node.objects.get(id=node_id) 260 | result = node.reboot() 261 | return HttpResponseRedirect('/overview/') 262 | 263 | @permission_required('provisioning.delete_node') 264 | def destroynode(request, node_id): 265 | node = Node.objects.get(id=node_id) 266 | result = node.destroy(request.user.username) 267 | return HttpResponseRedirect('/overview/') 268 | 269 | @login_required 270 | def settings(request): 271 | variables = RequestContext(request, { 272 | 'user_list': User.objects.all(), 273 | }) 274 | return render_to_response('settings.html', variables) 275 | 276 | def count_admin_users(): 277 | '''Returns the number of users belonging to the Admin group 278 | or having superuser rights''' 279 | g = get_object_or_404(Group, name='Admin') 280 | admin_users_count = len(g.user_set.all()) 281 | admin_users_count += len(User.objects.filter(is_superuser=True)) 282 | return admin_users_count 283 | 284 | @permission_required('auth.add_user') 285 | def adduser(request): 286 | if request.method == 'POST': 287 | form = UserCreationFormExtended(request.POST) 288 | if form.is_valid(): 289 | form.save() 290 | return HttpResponse('success
') 291 | else: 292 | form = UserCreationFormExtended() 293 | 294 | return render_to_response("registration/register.html", 295 | {'form': form, 'editing': False} 296 | ) 297 | 298 | @login_required 299 | def edituser(request, id): 300 | edit_user = get_object_or_404(User, id=id) 301 | if not request.user.has_perm('auth.change_user') and request.user.id != int(id): 302 | # If user doesn't have auth permissions and he/she is not editting 303 | # his/her own profile don't allow the operation 304 | return HttpResponse("Your don't have permissions to edit users
") 305 | 306 | if request.method == 'POST': 307 | if request.user.has_perm('auth.change_user'): 308 | admin = Group.objects.get(name='Admin') 309 | oldrole = admin if admin in edit_user.groups.all() else False 310 | newrole = Group.objects.get(id=request.POST.get('group')) 311 | if oldrole is admin and newrole != admin and count_admin_users() <= 1: 312 | errormsg = "Not allowed: you cannot remove admin rights" 313 | errormsg += " from the only admin user
" 314 | return HttpResponse(errormsg) 315 | form = UserEditForm(request.POST, instance=edit_user) 316 | else: 317 | form = ProfileEditForm(request.POST, instance=edit_user) 318 | 319 | if form.is_valid(): 320 | form.save() 321 | return HttpResponse('success
') 322 | else: 323 | if request.user.has_perm('auth.change_user'): 324 | form = UserEditForm(instance=edit_user) 325 | else: 326 | form = ProfileEditForm(instance=edit_user) 327 | 328 | variables = RequestContext(request, { 329 | 'form': form, 'editing': True, 'edit_user': edit_user 330 | }) 331 | return render_to_response("registration/register.html", variables) 332 | 333 | @permission_required('auth.delete_user') 334 | def deleteuser(request, id): 335 | user = get_object_or_404(User, id=id) 336 | if user.has_perm('auth.add_user') and count_admin_users() <= 1: 337 | return HttpResponse( 338 | "Not allowed: You cannot delete the only admin user
") 339 | user.delete() 340 | return HttpResponse('success
') 341 | -------------------------------------------------------------------------------- /overmind/api/test_provisioning.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import unittest 3 | import json 4 | 5 | from django.test import TestCase 6 | from django.test.client import Client 7 | from django.contrib.auth.models import User, Group 8 | from overmind.provisioning.models import Provider, Node 9 | 10 | 11 | class BaseProviderTestCase(TestCase): 12 | urls = 'overmind.test_urls' 13 | 14 | def setUp(self): 15 | self.path = "/api/providers/" 16 | 17 | op = Group.objects.get(name='Operator') 18 | self.user = User.objects.create_user( 19 | username='testuser', email='t@t.com', password='test1') 20 | self.user.groups.add(op) 21 | self.user.save() 22 | 23 | self.client = Client() 24 | #login = self.client.login( 25 | #username=self.user.username, password=self.user.password) 26 | #self.assertTrue(login) 27 | 28 | def create_provider(self): 29 | '''Utility function to create providers using the api''' 30 | data = { 31 | 'name': 'A provider to be updated', 32 | 'provider_type': 'DUMMY', 33 | 'access_key': 'somekey', 34 | } 35 | resp_new = self.client.post( 36 | self.path, json.dumps(data), content_type='application/json') 37 | self.assertEquals(resp_new.status_code, 200) 38 | return json.loads(resp_new.content) 39 | 40 | 41 | class ReadProviderTest(BaseProviderTestCase): 42 | def setUp(self): 43 | super(ReadProviderTest, self).setUp() 44 | 45 | self.p1 = Provider(name="prov1", provider_type="DUMMY", access_key="keyzz") 46 | self.p1.save() 47 | self.p2 = Provider(name="prov2", provider_type="DUMMY", access_key="keyzz2") 48 | self.p2.save() 49 | self.p3 = Provider(name="prov3", provider_type="dedicated") 50 | self.p3.save() 51 | 52 | def test_not_authenticated(self): 53 | """Should get a 401 when the user is not authenticated""" 54 | # NOTE: Use non-authenticated client 55 | response = self.client.get(self.path) 56 | self.assertEquals(response.status_code, 401) 57 | 58 | def test_get_all_providers(self): 59 | '''Should show all existing providers''' 60 | response = self.client.get(self.path) 61 | self.assertEquals(response.status_code, 200) 62 | expected = [ 63 | {'id': self.p1.id, 'access_key': self.p1.access_key, 64 | 'provider_type': self.p1.provider_type, 'name': self.p1.name}, 65 | {'id': self.p2.id, 'access_key': self.p2.access_key, 66 | 'provider_type': self.p2.provider_type, 'name': self.p2.name}, 67 | {'id': self.p3.id, 'access_key': self.p3.access_key, 68 | 'provider_type': self.p3.provider_type, 'name': self.p3.name}, 69 | ] 70 | self.assertEquals(json.loads(response.content), expected) 71 | 72 | def test_get_providers_by_type_dummy(self): 73 | '''Should show all providers of type DUMMY''' 74 | response = self.client.get(self.path + "?provider_type=DUMMY") 75 | self.assertEquals(response.status_code, 200) 76 | expected = [ 77 | {'id': self.p1.id, 'access_key': self.p1.access_key, 78 | 'provider_type': self.p1.provider_type, 'name': self.p1.name}, 79 | {'id': self.p2.id, 'access_key': self.p2.access_key, 80 | 'provider_type': self.p2.provider_type, 'name': self.p2.name}, 81 | ] 82 | self.assertEquals(json.loads(response.content), expected) 83 | 84 | def test_get_providers_by_type_dedicated(self): 85 | '''Should show all providers of type dedicated''' 86 | response = self.client.get(self.path + "?provider_type=dedicated") 87 | self.assertEquals(response.status_code, 200) 88 | expected = [ 89 | {'id': self.p3.id, 'access_key': self.p3.access_key, 90 | 'provider_type': self.p3.provider_type, 'name': self.p3.name}, 91 | ] 92 | self.assertEquals(json.loads(response.content), expected) 93 | 94 | def test_get_providers_by_type_not_found(self): 95 | '''Should show providers for non-existent type''' 96 | response = self.client.get(self.path + "?provider_type=DUMMIEST") 97 | self.assertEquals(response.status_code, 200) 98 | expected = [] 99 | self.assertEquals(json.loads(response.content), expected) 100 | 101 | def test_get_provider_by_id(self): 102 | '''Should show provider with id=2''' 103 | response = self.client.get(self.path + "2") 104 | self.assertEquals(response.status_code, 200) 105 | expected = { 106 | 'id': self.p2.id, 'access_key': self.p2.access_key, 107 | 'provider_type': self.p2.provider_type, 'name': self.p2.name, 108 | } 109 | self.assertEquals(json.loads(response.content), expected) 110 | 111 | def test_get_provider_by_id_not_found(self): 112 | '''Should return NOT_FOUND when requesting a provider with non existing id''' 113 | response = self.client.get(self.path + '99999') 114 | self.assertEquals(response.status_code, 404) 115 | 116 | def test_get_provider_by_name(self): 117 | '''Should show provider with name "prov1"''' 118 | response = self.client.get(self.path + "?name=prov1") 119 | self.assertEquals(response.status_code, 200) 120 | expected = { 121 | 'id': self.p1.id, 'access_key': self.p1.access_key, 122 | 'provider_type': self.p1.provider_type, 'name': self.p1.name 123 | } 124 | self.assertEquals(json.loads(response.content), expected) 125 | 126 | def test_get_provider_by_name_not_found(self): 127 | '''Should return NOT_FOUND when requesting a provider with a non existing name''' 128 | response = self.client.get(self.path + "?name=prov1nothere") 129 | self.assertEquals(response.status_code, 404) 130 | 131 | 132 | class CreateProviderTest(BaseProviderTestCase): 133 | def test_create_provider(self): 134 | '''Should create a new provider when request is valid''' 135 | data = { 136 | 'name': 'A new provider', 137 | 'provider_type': 'DUMMY', 138 | 'access_key': 'kiuuuuuu', 139 | } 140 | resp = self.client.post( 141 | self.path, json.dumps(data), content_type='application/json') 142 | self.assertEquals(resp.status_code, 200) 143 | 144 | expected = data 145 | expected["id"] = 1 146 | self.assertEquals(json.loads(resp.content), expected) 147 | 148 | #Check that it really is in the DB 149 | p = Provider.objects.get(id=1) 150 | self.assertEquals(p.name, 'A new provider') 151 | self.assertEquals(p.provider_type, 'DUMMY') 152 | 153 | def test_create_provider_should_import_nodes(self): 154 | '''Should import nodes when a new provider is created''' 155 | # There shouldn't be any nodes in the DB 156 | self.assertEquals(len(Node.objects.all()), 0) 157 | data = { 158 | 'name': 'A new provider', 159 | 'provider_type': 'DUMMY', 160 | 'access_key': 'kiuuuuuu', 161 | } 162 | resp = self.client.post( 163 | self.path, json.dumps(data), content_type='application/json') 164 | 165 | # There should be exactly 2 nodes in the DB now 166 | self.assertEquals(len(Node.objects.all()), 2) 167 | 168 | def test_create_provider_missing_access_key(self): 169 | """Should not create a new provider when access_key is missing""" 170 | data = {'name': 'A new provider', 'provider_type': 'DUMMY'} 171 | expected = "Bad Request\naccess_key: This field is required." 172 | resp = self.client.post( 173 | self.path, json.dumps(data), content_type='application/json') 174 | self.assertEquals(resp.status_code, 400) 175 | self.assertEquals(resp.content, expected) 176 | 177 | # Make sure it wasn't saved in the DB 178 | self.assertEquals(len(Provider.objects.all()), 0) 179 | 180 | def test_create_provider_empty_access_key(self): 181 | '''Should not create a new provider when access_key is empty''' 182 | data = {'name': 'A new provider', 183 | 'provider_type': 'DUMMY', 184 | 'access_key': '', 185 | } 186 | expected = "Bad Request\naccess_key: This field is required." 187 | resp = self.client.post( 188 | self.path, json.dumps(data), content_type='application/json') 189 | self.assertEquals(resp.status_code, 400) 190 | self.assertEquals(resp.content, expected) 191 | 192 | # Make sure it wasn't saved in the DB 193 | self.assertEquals(len(Provider.objects.all()), 0) 194 | 195 | 196 | class UpdateProviderTest(BaseProviderTestCase): 197 | def test_update_provider_name(self): 198 | '''Should update the provider name when request is valid''' 199 | # First create a provider 200 | new_data = self.create_provider() 201 | 202 | # Now update the newly added provider 203 | new_data['name'] = "ThisNameIsMuchBetter" 204 | resp = self.client.put( 205 | self.path + str(new_data["id"]), json.dumps(new_data), content_type='application/json') 206 | self.assertEquals(resp.status_code, 200) 207 | 208 | expected = new_data 209 | self.assertEquals(json.loads(resp.content), expected) 210 | 211 | #Check that it was also updated in the DB 212 | p = Provider.objects.get(id=new_data['id']) 213 | self.assertEquals(p.name, new_data['name']) 214 | 215 | def test_update_provider_missing_field(self): 216 | '''Should not update a provider when a field is missing''' 217 | # First create a provider 218 | new_data = self.create_provider() 219 | 220 | # Now try to update the provider while leaving out each field in turn 221 | for field in new_data: 222 | if field == "id": continue#field "id" is not required 223 | modified_data = copy.deepcopy(new_data)#Don't alter original data 224 | del modified_data[field]#remove a required field 225 | resp = self.client.put( 226 | self.path + str(new_data['id']), 227 | json.dumps(modified_data), 228 | content_type='application/json') 229 | expected = "Bad Request\n%s: This field is required." % field 230 | self.assertEquals(resp.status_code, 400) 231 | self.assertEquals(resp.content, expected) 232 | 233 | def test_update_provider_empty_field(self): 234 | """Should not update a provider when a field is empty""" 235 | # First create a provider 236 | new_data = self.create_provider() 237 | 238 | # Now try to update the provider while leaving out each field empty 239 | for field in new_data: 240 | if field == "id": continue#field "id" is not required 241 | modified_data = copy.deepcopy(new_data)#Don't alter original data 242 | modified_data[field] = ""#Make a field empty 243 | resp = self.client.put( 244 | self.path + str(new_data['id']), 245 | json.dumps(modified_data), 246 | content_type='application/json') 247 | expected = "Bad Request\n%s: This field is required." % field 248 | self.assertEquals(resp.status_code, 400) 249 | self.assertEquals(resp.content, expected) 250 | 251 | 252 | class DeleteProviderTest(BaseProviderTestCase): 253 | def test_delete_provider(self): 254 | '''Should delete a provider''' 255 | # First create a provider 256 | new_data = self.create_provider() 257 | 258 | # Now delete the newly added provider 259 | resp = self.client.delete(self.path + str(new_data['id'])) 260 | self.assertEquals(resp.status_code, 204) 261 | 262 | # Check that the api returns not found 263 | resp = self.client.get(self.path + str(new_data['id'])) 264 | self.assertEquals(resp.status_code, 404, 'The API should return NOT_FOUND') 265 | 266 | # Check that it was also deleted from the DB 267 | try: 268 | Provider.objects.get(id=new_data['id']) 269 | self.fail('The provider was not deleted from the DB') 270 | except Provider.DoesNotExist: 271 | pass 272 | 273 | 274 | class CreateImageTest(BaseProviderTestCase): 275 | def setUp(self): 276 | super(ReadImageTest, self).setUp() 277 | 278 | self.p1 = Provider(name="prov1", provider_type="DUMMY", access_key="keyzz") 279 | self.p1.save() 280 | self.p1.import_images() 281 | self.p2 = Provider(name="prov2", provider_type="DUMMY", access_key="keyzz2") 282 | self.p2.save() 283 | self.p2.import_images() 284 | 285 | def test_create_image_should_fail(self): 286 | '''Should return not allowed when trying to POST''' 287 | data = {"image_id": "10", "name": "myimage", "favorite": False, 288 | "provider_id": "1"} 289 | resp = self.client.post( 290 | self.path, json.dumps(data), content_type='application/json') 291 | self.assertEquals(response.status_code, 405) 292 | 293 | 294 | class ReadImageTest(BaseProviderTestCase): 295 | def setUp(self): 296 | super(ReadImageTest, self).setUp() 297 | 298 | self.p1 = Provider(name="prov1", provider_type="DUMMY", access_key="keyzz") 299 | self.p1.save() 300 | self.p1.import_images() 301 | self.p2 = Provider(name="prov2", provider_type="DUMMY", access_key="keyzz2") 302 | self.p2.save() 303 | self.p2.import_images() 304 | 305 | def test_get_all_images(self): 306 | '''Should return all images for a given provider''' 307 | response = self.client.get(self.path + str(self.p1.id) + "/images/") 308 | self.assertEquals(response.status_code, 200) 309 | expected = [ 310 | {"id": 1, "image_id": "1", "name": "Ubuntu 9.10", "favorite": False}, 311 | {"id": 2,"image_id": "2","name": "Ubuntu 9.04", "favorite": False}, 312 | {"id": 3, "image_id": "3", "name": "Slackware 4", "favorite": False}, 313 | ] 314 | self.assertEquals(json.loads(response.content), expected) 315 | 316 | def test_get_image_by_id(self): 317 | '''Should show image with id=2''' 318 | response = self.client.get(self.path + str(self.p1.id) + "/images/2") 319 | self.assertEquals(response.status_code, 200) 320 | expected = { 321 | "id": 2,"image_id": "2","name": "Ubuntu 9.04", "favorite": False} 322 | self.assertEquals(json.loads(response.content), expected) 323 | 324 | def test_get_image_by_image_id(self): 325 | '''Should show image with image_id=2''' 326 | path = self.path + str(self.p1.id) + "/images/" + "?image_id=2" 327 | response = self.client.get(path) 328 | self.assertEquals(response.status_code, 200) 329 | expected = { 330 | "id": 2,"image_id": "2","name": "Ubuntu 9.04", "favorite": False} 331 | self.assertEquals(json.loads(response.content), expected) 332 | 333 | def test_get_image_by_name(self): 334 | '''Should show image with name=Ubuntu 9.04''' 335 | path = self.path + str(self.p1.id) + "/images/" + "?name=Ubuntu 9.04" 336 | response = self.client.get(path) 337 | self.assertEquals(response.status_code, 200) 338 | expected = { 339 | "id": 2,"image_id": "2","name": "Ubuntu 9.04", "favorite": False} 340 | self.assertEquals(json.loads(response.content), expected) 341 | 342 | 343 | def suite(): 344 | suite = unittest.TestSuite() 345 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(CreateProviderTest)) 346 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(ReadProviderTest)) 347 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(UpdateProviderTest)) 348 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(DeleteProviderTest)) 349 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(ReadImageTest)) 350 | return suite 351 | -------------------------------------------------------------------------------- /overmind/provisioning/models.py: -------------------------------------------------------------------------------- 1 | import json 2 | import datetime 3 | import logging 4 | 5 | from IPy import IP 6 | from django.db import models, transaction 7 | 8 | from provisioning.controllers import ProviderController 9 | from provisioning.provider_meta import PROVIDERS 10 | 11 | 12 | provider_meta_keys = PROVIDERS.keys() 13 | provider_meta_keys.sort() 14 | PROVIDER_CHOICES = ([(key, key) for key in provider_meta_keys]) 15 | 16 | # libcloud states mapping 17 | STATES = { 18 | 0: 'Running', 19 | 1: 'Rebooting', 20 | 2: 'Terminated', 21 | 3: 'Pending', 22 | 4: 'Unknown', 23 | } 24 | 25 | 26 | def get_state(state): 27 | if state not in STATES: 28 | state = 4 29 | return STATES[state] 30 | 31 | 32 | class Action(models.Model): 33 | name = models.CharField(unique=True, max_length=20) 34 | show = models.BooleanField() 35 | 36 | def __unicode__(self): 37 | return self.name 38 | 39 | 40 | class Provider(models.Model): 41 | name = models.CharField(unique=True, max_length=25) 42 | provider_type = models.CharField( 43 | default='EC2_US_EAST', max_length=25, choices=PROVIDER_CHOICES) 44 | access_key = models.CharField("Access Key", max_length=100, blank=True) 45 | secret_key = models.CharField("Secret Key", max_length=100, blank=True) 46 | 47 | extra_param_name = models.CharField( 48 | "Extra parameter name", max_length=30, blank=True) 49 | extra_param_value = models.CharField( 50 | "Extra parameter value", max_length=30, blank=True) 51 | 52 | actions = models.ManyToManyField(Action) 53 | ready = models.BooleanField(default=False) 54 | conn = None 55 | 56 | class Meta: 57 | unique_together = ('provider_type', 'access_key') 58 | 59 | def save(self, *args, **kwargs): 60 | # Define proper key field names 61 | if PROVIDERS[self.provider_type]['access_key'] is not None: 62 | self._meta.get_field('access_key').verbose_name = \ 63 | PROVIDERS[self.provider_type]['access_key'] 64 | self._meta.get_field('access_key').blank = False 65 | if PROVIDERS[self.provider_type]['secret_key'] is not None: 66 | self._meta.get_field('secret_key').verbose_name = \ 67 | PROVIDERS[self.provider_type]['secret_key'] 68 | self._meta.get_field('access_key').blank = False 69 | 70 | # Read optional extra_param 71 | if 'extra_param' in PROVIDERS[self.provider_type].keys(): 72 | self.extra_param_name = PROVIDERS[self.provider_type]['extra_param'][0] 73 | self.extra_param_value = PROVIDERS[self.provider_type]['extra_param'][1] 74 | 75 | # Check connection and save new provider 76 | self.create_connection() 77 | # If connection was succesful save provider 78 | super(Provider, self).save(*args, **kwargs) 79 | logging.debug('Provider "%s" saved' % self.name) 80 | 81 | # Add supported actions 82 | for action_name in PROVIDERS[self.provider_type]['supported_actions']: 83 | try: 84 | action = Action.objects.get(name=action_name) 85 | except Action.DoesNotExist: 86 | raise Exception, 'Unsupported action "%s" specified' % action_name 87 | self.actions.add(action) 88 | 89 | def supports(self, action): 90 | try: 91 | self.actions.get(name=action) 92 | return True 93 | except Action.DoesNotExist: 94 | return False 95 | 96 | def create_connection(self): 97 | if self.conn is None: 98 | self.conn = ProviderController(self) 99 | 100 | @transaction.commit_on_success() 101 | def import_nodes(self): 102 | '''Sync nodes present at a provider with Overmind's DB''' 103 | if not self.supports('list'): return 104 | self.create_connection() 105 | nodes = self.conn.get_nodes() 106 | # Import nodes not present in the DB 107 | for node in nodes: 108 | try: 109 | n = Node.objects.get(provider=self, node_id=str(node.id)) 110 | except Node.DoesNotExist: 111 | # Create a new Node 112 | logging.info("import_nodes(): adding %s ..." % node) 113 | n = Node( 114 | name = node.name, 115 | node_id = str(node.id), 116 | provider = self, 117 | created_by = 'imported by Overmind', 118 | ) 119 | try: 120 | n.image = Image.objects.get( 121 | image_id=node.extra.get('imageId'), provider=self) 122 | except Image.DoesNotExist: 123 | n.image = None 124 | locs = Location.objects.filter(provider=self) 125 | if len(locs) == 1: 126 | n.location = locs[0] 127 | else: 128 | n.location = None 129 | try: 130 | size_id = node.extra.get('instancetype') or\ 131 | node.extra.get('flavorId') 132 | n.size = Size.objects.get(size_id=size_id, provider=self) 133 | except Size.DoesNotExist: 134 | n.size = None 135 | n.save() 136 | # Import/Update node info 137 | n.sync_ips(node.public_ips, public=True) 138 | n.sync_ips(node.private_ips, public=False) 139 | n.state = get_state(node.state) 140 | n.save_extra_data(node.extra) 141 | n.save() 142 | logging.debug("import_nodes(): succesfully saved %s" % node.name) 143 | 144 | # Delete nodes in the DB not listed by the provider 145 | for n in Node.objects.filter(provider=self 146 | ).exclude(environment='Decommissioned'): 147 | found = False 148 | for node in nodes: 149 | if n.node_id == str(node.id): 150 | found = True 151 | break 152 | # This node was probably removed from the provider by another tool 153 | # TODO: Needs user notification 154 | if not found: 155 | logging.info("import_nodes(): Delete node %s" % n) 156 | n.decommission() 157 | logging.debug("Finished synching nodes") 158 | 159 | @transaction.commit_on_success() 160 | def import_images(self): 161 | '''Get all images from this provider and store them in the DB 162 | The transaction.commit_on_success decorator is needed because 163 | some providers have thousands of images, which take a long time 164 | to save to the DB as separated transactions 165 | ''' 166 | if not self.supports('images'): return 167 | self.create_connection() 168 | for image in self.conn.get_images(): 169 | try: 170 | # Update image if it exists 171 | img = Image.objects.get(image_id=str(image.id), provider=self) 172 | except Image.DoesNotExist: 173 | # Create new image if it didn't exist 174 | img = Image( 175 | image_id = str(image.id), 176 | provider = self, 177 | ) 178 | img.name = image.name 179 | img.save() 180 | logging.debug( 181 | "Added new image '%s' for provider %s" % (img.name, self)) 182 | logging.info("Imported all images for provider %s" % self) 183 | 184 | @transaction.commit_on_success() 185 | def import_locations(self): 186 | '''Get all locations from this provider and store them in the DB''' 187 | if not self.supports('locations'): return 188 | self.create_connection() 189 | for location in self.conn.get_locations(): 190 | try: 191 | # Update location if it exists 192 | loc = Location.objects.get(location_id=str(location.id), provider=self) 193 | except Location.DoesNotExist: 194 | # Create new location if it didn't exist 195 | loc = Location( 196 | location_id = location.id, 197 | provider = self, 198 | ) 199 | loc.name = location.name 200 | loc.country = location.country 201 | loc.save() 202 | logging.debug( 203 | "Added new location '%s' for provider %s" % (loc.name, self)) 204 | logging.info("Imported all locations for provider %s" % self) 205 | 206 | @transaction.commit_on_success() 207 | def import_sizes(self): 208 | '''Get all sizes from this provider and store them in the DB''' 209 | if not self.supports('sizes'): return 210 | self.create_connection() 211 | sizes = self.conn.get_sizes() 212 | 213 | # Go through all sizes returned by the provider 214 | for size in sizes: 215 | try: 216 | # Read size 217 | s = Size.objects.get(size_id=str(size.id), provider=self) 218 | except Size.DoesNotExist: 219 | # Create new size if it didn't exist 220 | s = Size( 221 | size_id = str(size.id), 222 | provider = self, 223 | ) 224 | # Save/update size info 225 | s.name = size.name 226 | s.ram = size.ram 227 | s.disk = size.disk or "" 228 | s.bandwidth = size.bandwidth or "" 229 | s.price = size.price or "" 230 | s.save() 231 | logging.debug("Saved size '%s' for provider %s" % (s.name, self)) 232 | 233 | # Delete sizes in the DB not listed by the provider 234 | for s in self.get_sizes(): 235 | found = False 236 | for size in sizes: 237 | if s.size_id == str(size.id): 238 | found = True 239 | break 240 | # This size is probably not longer offered by the provider 241 | if not found: 242 | logging.debug("Deleted size %s" % s) 243 | s.delete() 244 | logging.debug("Finished synching sizes") 245 | 246 | def update(self): 247 | logging.debug('Updating provider "%s"...' % self.name) 248 | self.save() 249 | self.import_nodes() 250 | 251 | def check_credentials(self): 252 | if not self.supports('list'): return 253 | self.create_connection() 254 | self.conn.get_nodes() 255 | return True 256 | 257 | def get_sizes(self): 258 | return self.size_set.all() 259 | 260 | def get_images(self): 261 | return self.image_set.all() 262 | 263 | def get_fav_images(self): 264 | return self.image_set.filter(favorite=True).order_by('-last_used') 265 | 266 | def get_locations(self): 267 | return self.location_set.all() 268 | 269 | def create_node(self, data): 270 | self.create_connection() 271 | return self.conn.create_node(data) 272 | 273 | def reboot_node(self, node): 274 | self.create_connection() 275 | return self.conn.reboot_node(node) 276 | 277 | def destroy_node(self, node): 278 | self.create_connection() 279 | return self.conn.destroy_node(node) 280 | 281 | def __unicode__(self): 282 | return self.name 283 | 284 | 285 | class Image(models.Model): 286 | '''OS image model''' 287 | image_id = models.CharField(max_length=20) 288 | name = models.CharField(max_length=30) 289 | provider = models.ForeignKey(Provider) 290 | favorite = models.BooleanField(default=False) 291 | last_used = models.DateTimeField(auto_now=True) 292 | 293 | def __unicode__(self): 294 | return self.name 295 | 296 | class Meta: 297 | unique_together = ('provider', 'image_id') 298 | 299 | 300 | class Location(models.Model): 301 | '''Location model''' 302 | location_id = models.CharField(max_length=20) 303 | name = models.CharField(max_length=20) 304 | country = models.CharField(max_length=20) 305 | provider = models.ForeignKey(Provider) 306 | 307 | def __unicode__(self): 308 | return self.name 309 | 310 | class Meta: 311 | unique_together = ('provider', 'location_id') 312 | 313 | 314 | class Size(models.Model): 315 | '''Location model''' 316 | size_id = models.CharField(max_length=20) 317 | name = models.CharField(max_length=20) 318 | ram = models.CharField(max_length=20) 319 | disk = models.CharField(max_length=20) 320 | bandwidth = models.CharField(max_length=20, blank=True) 321 | price = models.CharField(max_length=20, blank=True) 322 | provider = models.ForeignKey(Provider) 323 | 324 | def __unicode__(self): 325 | return "%s (%sMB)" % (self.name, self.ram) 326 | 327 | class Meta: 328 | unique_together = ('provider', 'size_id') 329 | 330 | 331 | class NodeIP(models.Model): 332 | INET_FAMILIES = ( 333 | ('inet4', 4), 334 | ('inet6', 6), 335 | ) 336 | node = models.ForeignKey('Node', related_name='ips') 337 | address = models.IPAddressField() 338 | is_public = models.BooleanField(default=True) 339 | version = models.IntegerField(choices=INET_FAMILIES, default=4) 340 | position = models.IntegerField() 341 | interface_name = models.CharField(max_length=32, blank=True) 342 | 343 | def __unicode__(self): 344 | return "%s" % (self.address) 345 | 346 | class Node(models.Model): 347 | STATE_CHOICES = ( 348 | (u'Begin', u'Begin'), 349 | (u'Pending', u'Pending'), 350 | (u'Rebooting', u'Rebooting'), 351 | (u'Configuring', u'Configuring'), 352 | (u'Running', u'Running'), 353 | (u'Terminated', u'Terminated'), 354 | (u'Stopping', u'Stopping'), 355 | (u'Stopped', u'Stopped'), 356 | (u'Stranded', u'Stranded'), 357 | (u'Unknown', u'Unknown'), 358 | ) 359 | ENVIRONMENT_CHOICES = ( 360 | (u'Production', u'Production'), 361 | (u'Stage', u'Stage'), 362 | (u'Test', u'Test'), 363 | (u'Decommissioned', u'Decommissioned'), 364 | ) 365 | # Standard node fields 366 | name = models.CharField(max_length=25) 367 | node_id = models.CharField(max_length=50) 368 | provider = models.ForeignKey(Provider) 369 | image = models.ForeignKey(Image, null=True, blank=True) 370 | location = models.ForeignKey(Location, null=True, blank=True) 371 | size = models.ForeignKey(Size, null=True, blank=True) 372 | 373 | state = models.CharField( 374 | default='Begin', max_length=20, choices=STATE_CHOICES 375 | ) 376 | hostname = models.CharField(max_length=25, blank=True) 377 | _extra_data = models.TextField(blank=True) 378 | 379 | # Overmind related fields 380 | environment = models.CharField( 381 | default='Production', max_length=2, choices=ENVIRONMENT_CHOICES 382 | ) 383 | created_by = models.CharField(max_length=25) 384 | destroyed_by = models.CharField(max_length=25, blank=True) 385 | created_at = models.DateTimeField(auto_now_add=True) 386 | destroyed_at = models.DateTimeField(null=True) 387 | 388 | @property 389 | def public_ips(self): 390 | return self.ips.filter(is_public=True) 391 | 392 | 393 | @property 394 | def private_ips(self): 395 | return self.ips.filter(is_public=False) 396 | 397 | # Backward compatibility properties 398 | @property 399 | def public_ip(self): 400 | public_ips = self.ips.filter(is_public=True).filter(version=4) 401 | if len(public_ips): 402 | return public_ips[0].address 403 | return '' 404 | 405 | @property 406 | def private_ip(self): 407 | private_ips = self.ips.filter(is_public=False) 408 | if len(private_ips): 409 | return private_ips[0].address 410 | return '' 411 | 412 | # helper for related ips creation 413 | def sync_ips(self, ips, public=True): 414 | """Sync IP for a node""" 415 | previous = self.ips.filter(is_public=public).order_by('position') 416 | addrs = [] 417 | for i in ips: 418 | addr = IP(i) 419 | addrs.append(addr) 420 | new_ips = [x.strFullsize() for x in addrs] 421 | for p in previous: 422 | if p.address in new_ips: 423 | p.position = new_ips.index(p.address) 424 | else: 425 | p.delete() 426 | for a in addrs: 427 | if a.strFullsize() not in [x.address for x in previous]: 428 | # Create new nodeip object 429 | NodeIP.objects.create( 430 | address=a.strFullsize(), 431 | position=new_ips.index(a.strFullsize()), 432 | version=a.version(), is_public=public, node=self 433 | ) 434 | 435 | class Meta: 436 | unique_together = (('provider', 'name'), ('provider', 'node_id')) 437 | 438 | def __unicode__(self): 439 | return "<" + str(self.provider) + ": " + self.name + " - " + self.public_ip + " - " + str(self.node_id) + ">" 440 | 441 | def save_extra_data(self, data): 442 | self._extra_data = json.dumps(data) 443 | 444 | def extra_data(self): 445 | if self._extra_data == '': 446 | return {} 447 | return json.loads(self._extra_data) 448 | 449 | def reboot(self): 450 | '''Returns True if the reboot was successful, otherwise False''' 451 | if not self.provider.supports('reboot'): 452 | return True 453 | 454 | ret = self.provider.reboot_node(self) 455 | if ret: 456 | logging.debug('Rebooted %s' % self) 457 | else: 458 | logging.warn('Could not reboot node %s' % self) 459 | return ret 460 | 461 | def destroy(self, username): 462 | '''Returns True if the destroy was successful, otherwise False''' 463 | if self.provider.supports('destroy'): 464 | ret = self.provider.destroy_node(self) 465 | if ret: 466 | logging.info('Destroyed %s' % self) 467 | else: 468 | logging.error("controler.destroy_node() did not return True: %s.\nnot calling Node.delete()" % ret) 469 | return False 470 | self.decommission() 471 | self.destroyed_by = username 472 | self.destroyed_at = datetime.datetime.now() 473 | self.save() 474 | return True 475 | 476 | def decommission(self): 477 | '''Rename node and set its environment to decomissioned''' 478 | self.state = 'Terminated' 479 | # Rename node to free the name for future use 480 | counter = 1 481 | newname = "DECOM" + str(counter) + "-" + self.name 482 | while(len(Node.objects.filter( 483 | provider=self.provider,name=newname 484 | ).exclude( 485 | id=self.id 486 | ))): 487 | counter += 1 488 | newname = "DECOM" + str(counter) + "-" + self.name 489 | self.name = newname 490 | 491 | # Mark as decommissioned and save 492 | self.environment = 'Decommissioned' 493 | self.save() 494 | -------------------------------------------------------------------------------- /overmind/media/js/jquery.dimensions.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2007 Paul Bakaus (paul.bakaus@googlemail.com) and Brandon Aaron (brandon.aaron@gmail.com || http://brandonaaron.net) 2 | * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) 3 | * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. 4 | * 5 | * $LastChangedDate: 2007-06-22 04:38:37 +0200 (Fr, 22 Jun 2007) $ 6 | * $Rev: 2141 $ 7 | * 8 | * Version: 1.0b2 9 | */ 10 | 11 | (function($){ 12 | 13 | // store a copy of the core height and width methods 14 | var height = $.fn.height, 15 | width = $.fn.width; 16 | 17 | $.fn.extend({ 18 | /** 19 | * If used on document, returns the document's height (innerHeight) 20 | * If used on window, returns the viewport's (window) height 21 | * See core docs on height() to see what happens when used on an element. 22 | * 23 | * @example $("#testdiv").height() 24 | * @result 200 25 | * 26 | * @example $(document).height() 27 | * @result 800 28 | * 29 | * @example $(window).height() 30 | * @result 400 31 | * 32 | * @name height 33 | * @type Object 34 | * @cat Plugins/Dimensions 35 | */ 36 | height: function() { 37 | if ( this[0] == window ) 38 | return self.innerHeight || 39 | $.boxModel && document.documentElement.clientHeight || 40 | document.body.clientHeight; 41 | 42 | if ( this[0] == document ) 43 | return Math.max( document.body.scrollHeight, document.body.offsetHeight ); 44 | 45 | return height.apply(this, arguments); 46 | }, 47 | 48 | /** 49 | * If used on document, returns the document's width (innerWidth) 50 | * If used on window, returns the viewport's (window) width 51 | * See core docs on height() to see what happens when used on an element. 52 | * 53 | * @example $("#testdiv").width() 54 | * @result 200 55 | * 56 | * @example $(document).width() 57 | * @result 800 58 | * 59 | * @example $(window).width() 60 | * @result 400 61 | * 62 | * @name width 63 | * @type Object 64 | * @cat Plugins/Dimensions 65 | */ 66 | width: function() { 67 | if ( this[0] == window ) 68 | return self.innerWidth || 69 | $.boxModel && document.documentElement.clientWidth || 70 | document.body.clientWidth; 71 | 72 | if ( this[0] == document ) 73 | return Math.max( document.body.scrollWidth, document.body.offsetWidth ); 74 | 75 | return width.apply(this, arguments); 76 | }, 77 | 78 | /** 79 | * Returns the inner height value (without border) for the first matched element. 80 | * If used on document, returns the document's height (innerHeight) 81 | * If used on window, returns the viewport's (window) height 82 | * 83 | * @example $("#testdiv").innerHeight() 84 | * @result 800 85 | * 86 | * @name innerHeight 87 | * @type Number 88 | * @cat Plugins/Dimensions 89 | */ 90 | innerHeight: function() { 91 | return this[0] == window || this[0] == document ? 92 | this.height() : 93 | this.is(':visible') ? 94 | this[0].offsetHeight - num(this, 'borderTopWidth') - num(this, 'borderBottomWidth') : 95 | this.height() + num(this, 'paddingTop') + num(this, 'paddingBottom'); 96 | }, 97 | 98 | /** 99 | * Returns the inner width value (without border) for the first matched element. 100 | * If used on document, returns the document's Width (innerWidth) 101 | * If used on window, returns the viewport's (window) width 102 | * 103 | * @example $("#testdiv").innerWidth() 104 | * @result 1000 105 | * 106 | * @name innerWidth 107 | * @type Number 108 | * @cat Plugins/Dimensions 109 | */ 110 | innerWidth: function() { 111 | return this[0] == window || this[0] == document ? 112 | this.width() : 113 | this.is(':visible') ? 114 | this[0].offsetWidth - num(this, 'borderLeftWidth') - num(this, 'borderRightWidth') : 115 | this.width() + num(this, 'paddingLeft') + num(this, 'paddingRight'); 116 | }, 117 | 118 | /** 119 | * Returns the outer height value (including border) for the first matched element. 120 | * Cannot be used on document or window. 121 | * 122 | * @example $("#testdiv").outerHeight() 123 | * @result 1000 124 | * 125 | * @name outerHeight 126 | * @type Number 127 | * @cat Plugins/Dimensions 128 | */ 129 | outerHeight: function() { 130 | return this[0] == window || this[0] == document ? 131 | this.height() : 132 | this.is(':visible') ? 133 | this[0].offsetHeight : 134 | this.height() + num(this,'borderTopWidth') + num(this, 'borderBottomWidth') + num(this, 'paddingTop') + num(this, 'paddingBottom'); 135 | }, 136 | 137 | /** 138 | * Returns the outer width value (including border) for the first matched element. 139 | * Cannot be used on document or window. 140 | * 141 | * @example $("#testdiv").outerHeight() 142 | * @result 1000 143 | * 144 | * @name outerHeight 145 | * @type Number 146 | * @cat Plugins/Dimensions 147 | */ 148 | outerWidth: function() { 149 | return this[0] == window || this[0] == document ? 150 | this.width() : 151 | this.is(':visible') ? 152 | this[0].offsetWidth : 153 | this.width() + num(this, 'borderLeftWidth') + num(this, 'borderRightWidth') + num(this, 'paddingLeft') + num(this, 'paddingRight'); 154 | }, 155 | 156 | /** 157 | * Returns how many pixels the user has scrolled to the right (scrollLeft). 158 | * Works on containers with overflow: auto and window/document. 159 | * 160 | * @example $("#testdiv").scrollLeft() 161 | * @result 100 162 | * 163 | * @name scrollLeft 164 | * @type Number 165 | * @cat Plugins/Dimensions 166 | */ 167 | /** 168 | * Sets the scrollLeft property and continues the chain. 169 | * Works on containers with overflow: auto and window/document. 170 | * 171 | * @example $("#testdiv").scrollLeft(10).scrollLeft() 172 | * @result 10 173 | * 174 | * @name scrollLeft 175 | * @param Number value A positive number representing the desired scrollLeft. 176 | * @type jQuery 177 | * @cat Plugins/Dimensions 178 | */ 179 | scrollLeft: function(val) { 180 | if ( val != undefined ) 181 | // set the scroll left 182 | return this.each(function() { 183 | if (this == window || this == document) 184 | window.scrollTo( val, $(window).scrollTop() ); 185 | else 186 | this.scrollLeft = val; 187 | }); 188 | 189 | // return the scroll left offest in pixels 190 | if ( this[0] == window || this[0] == document ) 191 | return self.pageXOffset || 192 | $.boxModel && document.documentElement.scrollLeft || 193 | document.body.scrollLeft; 194 | 195 | return this[0].scrollLeft; 196 | }, 197 | 198 | /** 199 | * Returns how many pixels the user has scrolled to the bottom (scrollTop). 200 | * Works on containers with overflow: auto and window/document. 201 | * 202 | * @example $("#testdiv").scrollTop() 203 | * @result 100 204 | * 205 | * @name scrollTop 206 | * @type Number 207 | * @cat Plugins/Dimensions 208 | */ 209 | /** 210 | * Sets the scrollTop property and continues the chain. 211 | * Works on containers with overflow: auto and window/document. 212 | * 213 | * @example $("#testdiv").scrollTop(10).scrollTop() 214 | * @result 10 215 | * 216 | * @name scrollTop 217 | * @param Number value A positive number representing the desired scrollTop. 218 | * @type jQuery 219 | * @cat Plugins/Dimensions 220 | */ 221 | scrollTop: function(val) { 222 | if ( val != undefined ) 223 | // set the scroll top 224 | return this.each(function() { 225 | if (this == window || this == document) 226 | window.scrollTo( $(window).scrollLeft(), val ); 227 | else 228 | this.scrollTop = val; 229 | }); 230 | 231 | // return the scroll top offset in pixels 232 | if ( this[0] == window || this[0] == document ) 233 | return self.pageYOffset || 234 | $.boxModel && document.documentElement.scrollTop || 235 | document.body.scrollTop; 236 | 237 | return this[0].scrollTop; 238 | }, 239 | 240 | /** 241 | * Returns the top and left positioned offset in pixels. 242 | * The positioned offset is the offset between a positioned 243 | * parent and the element itself. 244 | * 245 | * @example $("#testdiv").position() 246 | * @result { top: 100, left: 100 } 247 | * 248 | * @name position 249 | * @param Map options Optional settings to configure the way the offset is calculated. 250 | * @option Boolean margin Should the margin of the element be included in the calculations? False by default. 251 | * @option Boolean border Should the border of the element be included in the calculations? False by default. 252 | * @option Boolean padding Should the padding of the element be included in the calculations? False by default. 253 | * @param Object returnObject An object to store the return value in, so as not to break the chain. If passed in the 254 | * chain will not be broken and the result will be assigned to this object. 255 | * @type Object 256 | * @cat Plugins/Dimensions 257 | */ 258 | position: function(options, returnObject) { 259 | var elem = this[0], parent = elem.parentNode, op = elem.offsetParent, 260 | options = $.extend({ margin: false, border: false, padding: false, scroll: false }, options || {}), 261 | x = elem.offsetLeft, 262 | y = elem.offsetTop, 263 | sl = elem.scrollLeft, 264 | st = elem.scrollTop; 265 | 266 | // Mozilla and IE do not add the border 267 | if ($.browser.mozilla || $.browser.msie) { 268 | // add borders to offset 269 | x += num(elem, 'borderLeftWidth'); 270 | y += num(elem, 'borderTopWidth'); 271 | } 272 | 273 | if ($.browser.mozilla) { 274 | do { 275 | // Mozilla does not add the border for a parent that has overflow set to anything but visible 276 | if ($.browser.mozilla && parent != elem && $.css(parent, 'overflow') != 'visible') { 277 | x += num(parent, 'borderLeftWidth'); 278 | y += num(parent, 'borderTopWidth'); 279 | } 280 | 281 | if (parent == op) break; // break if we are already at the offestParent 282 | } while ((parent = parent.parentNode) && (parent.tagName.toLowerCase() != 'body' || parent.tagName.toLowerCase() != 'html')); 283 | } 284 | 285 | var returnValue = handleOffsetReturn(elem, options, x, y, sl, st); 286 | 287 | if (returnObject) { $.extend(returnObject, returnValue); return this; } 288 | else { return returnValue; } 289 | }, 290 | 291 | /** 292 | * Returns the location of the element in pixels from the top left corner of the viewport. 293 | * 294 | * For accurate readings make sure to use pixel values for margins, borders and padding. 295 | * 296 | * Known issues: 297 | * - Issue: A div positioned relative or static without any content before it and its parent will report an offsetTop of 0 in Safari 298 | * Workaround: Place content before the relative div ... and set height and width to 0 and overflow to hidden 299 | * 300 | * @example $("#testdiv").offset() 301 | * @result { top: 100, left: 100, scrollTop: 10, scrollLeft: 10 } 302 | * 303 | * @example $("#testdiv").offset({ scroll: false }) 304 | * @result { top: 90, left: 90 } 305 | * 306 | * @example var offset = {} 307 | * $("#testdiv").offset({ scroll: false }, offset) 308 | * @result offset = { top: 90, left: 90 } 309 | * 310 | * @name offset 311 | * @param Map options Optional settings to configure the way the offset is calculated. 312 | * @option Boolean margin Should the margin of the element be included in the calculations? True by default. 313 | * @option Boolean border Should the border of the element be included in the calculations? False by default. 314 | * @option Boolean padding Should the padding of the element be included in the calculations? False by default. 315 | * @option Boolean scroll Should the scroll offsets of the parent elements be included in the calculations? True by default. 316 | * When true it adds the totla scroll offets of all parents to the total offset and also adds two properties 317 | * to the returned object, scrollTop and scrollLeft. 318 | * @options Boolean lite Will use offsetLite instead of offset when set to true. False by default. 319 | * @param Object returnObject An object to store the return value in, so as not to break the chain. If passed in the 320 | * chain will not be broken and the result will be assigned to this object. 321 | * @type Object 322 | * @cat Plugins/Dimensions 323 | */ 324 | offset: function(options, returnObject) { 325 | var x = 0, y = 0, sl = 0, st = 0, 326 | elem = this[0], parent = this[0], op, parPos, elemPos = $.css(elem, 'position'), 327 | mo = $.browser.mozilla, ie = $.browser.msie, sf = $.browser.safari, oa = $.browser.opera, 328 | absparent = false, relparent = false, 329 | options = $.extend({ margin: true, border: false, padding: false, scroll: true, lite: false }, options || {}); 330 | 331 | // Use offsetLite if lite option is true 332 | if (options.lite) return this.offsetLite(options, returnObject); 333 | 334 | if (elem.tagName.toLowerCase() == 'body') { 335 | // Safari is the only one to get offsetLeft and offsetTop properties of the body "correct" 336 | // Except they all mess up when the body is positioned absolute or relative 337 | x = elem.offsetLeft; 338 | y = elem.offsetTop; 339 | // Mozilla ignores margin and subtracts border from body element 340 | if (mo) { 341 | x += num(elem, 'marginLeft') + (num(elem, 'borderLeftWidth')*2); 342 | y += num(elem, 'marginTop') + (num(elem, 'borderTopWidth') *2); 343 | } else 344 | // Opera ignores margin 345 | if (oa) { 346 | x += num(elem, 'marginLeft'); 347 | y += num(elem, 'marginTop'); 348 | } else 349 | // IE does not add the border in Standards Mode 350 | if (ie && jQuery.boxModel) { 351 | x += num(elem, 'borderLeftWidth'); 352 | y += num(elem, 'borderTopWidth'); 353 | } 354 | } else { 355 | do { 356 | parPos = $.css(parent, 'position'); 357 | 358 | x += parent.offsetLeft; 359 | y += parent.offsetTop; 360 | 361 | // Mozilla and IE do not add the border 362 | if (mo || ie) { 363 | // add borders to offset 364 | x += num(parent, 'borderLeftWidth'); 365 | y += num(parent, 'borderTopWidth'); 366 | 367 | // Mozilla does not include the border on body if an element isn't positioned absolute and is without an absolute parent 368 | if (mo && parPos == 'absolute') absparent = true; 369 | // IE does not include the border on the body if an element is position static and without an absolute or relative parent 370 | if (ie && parPos == 'relative') relparent = true; 371 | } 372 | 373 | op = parent.offsetParent; 374 | if (options.scroll || mo) { 375 | do { 376 | if (options.scroll) { 377 | // get scroll offsets 378 | sl += parent.scrollLeft; 379 | st += parent.scrollTop; 380 | } 381 | 382 | // Mozilla does not add the border for a parent that has overflow set to anything but visible 383 | if (mo && parent != elem && $.css(parent, 'overflow') != 'visible') { 384 | x += num(parent, 'borderLeftWidth'); 385 | y += num(parent, 'borderTopWidth'); 386 | } 387 | 388 | parent = parent.parentNode; 389 | } while (parent != op); 390 | } 391 | parent = op; 392 | 393 | if (parent.tagName.toLowerCase() == 'body' || parent.tagName.toLowerCase() == 'html') { 394 | // Safari and IE Standards Mode doesn't add the body margin for elments positioned with static or relative 395 | if ((sf || (ie && $.boxModel)) && elemPos != 'absolute' && elemPos != 'fixed') { 396 | x += num(parent, 'marginLeft'); 397 | y += num(parent, 'marginTop'); 398 | } 399 | // Mozilla does not include the border on body if an element isn't positioned absolute and is without an absolute parent 400 | // IE does not include the border on the body if an element is positioned static and without an absolute or relative parent 401 | if ( (mo && !absparent && elemPos != 'fixed') || 402 | (ie && elemPos == 'static' && !relparent) ) { 403 | x += num(parent, 'borderLeftWidth'); 404 | y += num(parent, 'borderTopWidth'); 405 | } 406 | break; // Exit the loop 407 | } 408 | } while (parent); 409 | } 410 | 411 | var returnValue = handleOffsetReturn(elem, options, x, y, sl, st); 412 | 413 | if (returnObject) { $.extend(returnObject, returnValue); return this; } 414 | else { return returnValue; } 415 | }, 416 | 417 | /** 418 | * Returns the location of the element in pixels from the top left corner of the viewport. 419 | * This method is much faster than offset but not as accurate. This method can be invoked 420 | * by setting the lite option to true in the offset method. 421 | * 422 | * @name offsetLite 423 | * @param Map options Optional settings to configure the way the offset is calculated. 424 | * @option Boolean margin Should the margin of the element be included in the calculations? True by default. 425 | * @option Boolean border Should the border of the element be included in the calculations? False by default. 426 | * @option Boolean padding Should the padding of the element be included in the calculations? False by default. 427 | * @option Boolean scroll Should the scroll offsets of the parent elements be included in the calculations? True by default. 428 | * When true it adds the totla scroll offets of all parents to the total offset and also adds two properties 429 | * to the returned object, scrollTop and scrollLeft. 430 | * @param Object returnObject An object to store the return value in, so as not to break the chain. If passed in the 431 | * chain will not be broken and the result will be assigned to this object. 432 | * @type Object 433 | * @cat Plugins/Dimensions 434 | */ 435 | offsetLite: function(options, returnObject) { 436 | var x = 0, y = 0, sl = 0, st = 0, parent = this[0], op, 437 | options = $.extend({ margin: true, border: false, padding: false, scroll: true }, options || {}); 438 | 439 | do { 440 | x += parent.offsetLeft; 441 | y += parent.offsetTop; 442 | 443 | op = parent.offsetParent; 444 | if (options.scroll) { 445 | // get scroll offsets 446 | do { 447 | sl += parent.scrollLeft; 448 | st += parent.scrollTop; 449 | parent = parent.parentNode; 450 | } while(parent != op); 451 | } 452 | parent = op; 453 | } while (parent && parent.tagName.toLowerCase() != 'body' && parent.tagName.toLowerCase() != 'html'); 454 | 455 | var returnValue = handleOffsetReturn(this[0], options, x, y, sl, st); 456 | 457 | if (returnObject) { $.extend(returnObject, returnValue); return this; } 458 | else { return returnValue; } 459 | } 460 | }); 461 | 462 | /** 463 | * Handles converting a CSS Style into an Integer. 464 | * @private 465 | */ 466 | var num = function(el, prop) { 467 | return parseInt($.css(el.jquery?el[0]:el,prop))||0; 468 | }; 469 | 470 | /** 471 | * Handles the return value of the offset and offsetLite methods. 472 | * @private 473 | */ 474 | var handleOffsetReturn = function(elem, options, x, y, sl, st) { 475 | if ( !options.margin ) { 476 | x -= num(elem, 'marginLeft'); 477 | y -= num(elem, 'marginTop'); 478 | } 479 | 480 | // Safari and Opera do not add the border for the element 481 | if ( options.border && ($.browser.safari || $.browser.opera) ) { 482 | x += num(elem, 'borderLeftWidth'); 483 | y += num(elem, 'borderTopWidth'); 484 | } else if ( !options.border && !($.browser.safari || $.browser.opera) ) { 485 | x -= num(elem, 'borderLeftWidth'); 486 | y -= num(elem, 'borderTopWidth'); 487 | } 488 | 489 | if ( options.padding ) { 490 | x += num(elem, 'paddingLeft'); 491 | y += num(elem, 'paddingTop'); 492 | } 493 | 494 | // do not include scroll offset on the element 495 | if ( options.scroll ) { 496 | sl -= elem.scrollLeft; 497 | st -= elem.scrollTop; 498 | } 499 | 500 | return options.scroll ? { top: y - st, left: x - sl, scrollTop: st, scrollLeft: sl } 501 | : { top: y, left: x }; 502 | }; 503 | 504 | })(jQuery); -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc.