├── 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 |

Cancel

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 |
12 |

13 |

14 |

15 |

16 | 17 |

18 |

19 |
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/(?P\d+)/images/$', image_resource), 16 | url(r'^providers/(?P\d+)/images/(?P\d+)$', image_resource), 17 | url(r'^providers/$', provider_resource), 18 | url(r'^providers/(?P\d+)$', provider_resource), 19 | url(r'^nodes/$', node_resource), 20 | url(r'^nodes/(?P\d+)$', node_resource), 21 | ) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | All files in this work, are now covered by the following copyright notice. 2 | 3 | Copyright (c) 2010 Miquel Torres and Grig Gheorghiu 4 | 5 | This file is part of Overmind. 6 | 7 | Overmind is free software; you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | 13 | Overmind is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with Overmind. If not, see . 20 | 21 | The full text of the GPL is in the COPYING file. 22 | -------------------------------------------------------------------------------- /overmind/provisioning/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | # Provisioning plugins module 2 | 3 | def get_driver(provider): 4 | """Gets a driver 5 | @param provider: name of provider to get driver 6 | """ 7 | _mod = __import__(provider, globals(), locals()) 8 | return getattr(_mod, "Driver") 9 | 10 | def load_plugins(): 11 | import os 12 | plugin_list = {} 13 | for f in os.listdir(os.path.dirname(__file__)): 14 | if f.endswith('.py') and f != '__init__.py' and f != 'providerplugin.py': 15 | driver_name = f.rstrip('.py') 16 | _mod = __import__(driver_name, globals(), locals()) 17 | 18 | meta = { 19 | 'display_name': _mod.display_name, 20 | 'access_key': _mod.access_key, 21 | 'secret_key': _mod.secret_key, 22 | 'plugin' : True, 23 | 'form_fields': _mod.form_fields, 24 | 'supported_actions': _mod.supported_actions, 25 | } 26 | plugin_list[driver_name] = meta 27 | return plugin_list 28 | -------------------------------------------------------------------------------- /overmind/templates/node_form.html: -------------------------------------------------------------------------------- 1 | {% for field in form.visible_fields %} 2 | {{ field.errors }} 3 |
{# Include the hidden fields in the form #} 4 | {% if forloop.first %} 5 | {% for hidden in form.hidden_fields %} 6 | {{ hidden }} 7 | {% endfor %} 8 | {% endif %} 9 | {{ field.label_tag }}: {{ field }} 10 |
{% if field.html_name == "image" %} 11 |

Add new image

12 |
13 | 20 | {% endif %} 21 | {% endfor %} 22 | 23 | 24 | {% if error %}

Error: {{ 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\d+)/images/$', image_resource), 23 | url(r'^providers/(?P\d+)/images/(?P\d+)$', image_resource), 24 | url(r'^providers/$', provider_resource), 25 | url(r'^providers/(?P\d+)$', provider_resource), 26 | url(r'^nodes/$', node_resource), 27 | url(r'^nodes/(?P\d+)$', node_resource), 28 | ) 29 | -------------------------------------------------------------------------------- /overmind/templates/provider.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}: Add a Provider{% endblock %} 3 | {% block script %} 4 | 22 | {% endblock %} 23 | {% block sidebar %}{% endblock %} 24 | 25 | {% block content %} 26 |
27 |

Add a new Provider

28 |
    29 |
  • 30 |

    Provider

    31 |
      {% for provider in provider_types %} 32 |
    • 33 | 34 | 35 |
    • {% endfor %} 36 |
    37 |
  • 38 |
39 | 40 |
41 |
42 |
43 | {% endblock %} 44 | 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overmind 2 | 3 | This project aims to provider a complete server provisioning and configuration management application. 4 | 5 | The first version is a *unified front-end* to public and private clouds, custom server providers and dedicated hardware. 6 | 7 | ## Features 8 | 9 | * EC2 and Rackspace server provisioning. All clouds supported by libcloud will be supported given enough testing 10 | * Provider Plugins: Any provider can be integrated by writing either a libcloud driver or an Overmind provisioning plugin 11 | * Import any server into Overmind witht the "Dedicated Hardware" plugin 12 | * Complete REST API for provider and nodes 13 | * Authentication with three user roles 14 | 15 | See the wiki for architectural info. 16 | 17 | ## Installation 18 | 19 | ### Requirements 20 | 21 | * Python 2.6+ 22 | * Django 1.3 23 | * apache-libcloud 24 | * django-celery 25 | * RabbitMQ (or alternative message queue supported by Celery) 26 | 27 | All python dependencies can be installed using the requirements file: 28 | 29 | $ pip install -r requirements.txt 30 | 31 | ### Install Overmind 32 | 33 | * Download the last stable release from 34 | [http://github.com/tobami/overmind/downloads](http://github.com/tobami/overmind/downloads) 35 | and unpack it 36 | * Create the DB by changing to the `overmind/` directory and running: 37 | 38 | python manage.py syncdb 39 | 40 | * For testing purposes start the celery server on a console 41 | 42 | python manage.py celeryd -l info 43 | 44 | and the django development server 45 | 46 | python manage.py runserver 47 | 48 | Now you can visit the Overmind overview page on `localhost:8000/overview` 49 | -------------------------------------------------------------------------------- /overmind/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Overmind{% block title %}{% endblock %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | {% block script %}{% endblock %} 16 | 17 | 18 |
19 |
20 |

Overmind

21 | 29 | {% if user.is_authenticated %}

{{ user.username }} | Logout

{% endif %} 30 |
31 |
32 |
{% block sidebar %}{% endblock %} 33 |
{% block content %}{% endblock %}
34 |
35 |
{% block footer %}{% endblock %}
36 | 37 | 38 | -------------------------------------------------------------------------------- /overmind/provisioning/plugins/dedicated.py: -------------------------------------------------------------------------------- 1 | # Dedicated Hardware plugin 2 | from libcloud.compute.base import ConnectionKey, NodeDriver, Node 3 | from libcloud.compute.types import NodeState 4 | 5 | display_name = "Dedicated Hardware" 6 | access_key = None 7 | secret_key = None 8 | form_fields = ['ip'] 9 | supported_actions = ['create'] 10 | 11 | 12 | class Connection(ConnectionKey): 13 | '''Dummy connection''' 14 | def connect(self, host=None, port=None): 15 | pass 16 | 17 | 18 | class Driver(NodeDriver): 19 | name = display_name 20 | type = 0 21 | 22 | def __init__(self, creds): 23 | self.creds = creds 24 | self.connection = Connection(self.creds) 25 | 26 | def _validate_ip(self, ip): 27 | try: 28 | # Validate with IPy module 29 | from IPy import IP 30 | try: 31 | IP(ip) 32 | except ValueError: 33 | raise Exception, "Incorrect IP" 34 | except ImportError: 35 | # Validate with regex 36 | import re 37 | valid_ip_regex = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$" 38 | if re.match(valid_ip_regex, ip) is None: 39 | raise Exception, "Incorrect IP" 40 | 41 | def create_node(self, **kwargs): 42 | # Validate IP address 43 | ip = kwargs.get('ip', '') 44 | self._validate_ip(ip) 45 | 46 | # Return Node object (IP serves as id feed) 47 | n = Node(id=ip.replace(".",""), 48 | name=kwargs.get('name'), 49 | state=NodeState.RUNNING, 50 | public_ip=[ip], 51 | private_ip=[], 52 | driver=self) 53 | return n 54 | -------------------------------------------------------------------------------- /overmind/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from django.views.generic.simple import redirect_to 3 | from django.conf import settings 4 | 5 | 6 | urlpatterns = [] 7 | 8 | if settings.DEBUG: 9 | urlpatterns = patterns('', 10 | (r'^media/(?P.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT}), 11 | ) 12 | 13 | urlpatterns += patterns('', 14 | (r'^api/', include('overmind.api.urls')), 15 | ) 16 | 17 | # Provisioning 18 | urlpatterns += patterns('', 19 | (r'^overview/$', 'provisioning.views.overview'), 20 | (r'^provider/$', 'provisioning.views.provider'), 21 | (r'^node/$', 'provisioning.views.node'), 22 | (r'^settings/$', 'provisioning.views.settings'), 23 | 24 | # Create 25 | (r'^provider/new/$', 'provisioning.views.newprovider'), 26 | (r'^node/new/$', 'provisioning.views.newnode'), 27 | (r'^node/image/add/$', 'provisioning.views.addimage'), 28 | (r'^node/image/(?P\d+)/remove/$', 'provisioning.views.removeimage'), 29 | 30 | # Update 31 | (r'^provider/update/$', 'provisioning.views.updateproviders'), 32 | 33 | # Reboot 34 | (r'^node/(?P\d+)/reboot/$', 'provisioning.views.rebootnode'), 35 | 36 | # Delete 37 | (r'^provider/(?P\d+)/delete/$',\ 38 | 'provisioning.views.deleteprovider'), 39 | (r'^node/(?P\d+)/destroy/$', 'provisioning.views.destroynode'), 40 | 41 | (r'^$', redirect_to, {'url': '/overview/', 'permanent': False}), 42 | ) 43 | 44 | # Users 45 | urlpatterns += patterns('', 46 | (r'^accounts/login/$', 'django.contrib.auth.views.login'), 47 | (r'^accounts/logout/$', 'django.contrib.auth.views.logout'), 48 | (r'^accounts/new/$', 'provisioning.views.adduser'), 49 | (r'^accounts/edit/(?P\d+)/$', 'provisioning.views.edituser'), 50 | (r'^accounts/delete/(?P\d+)/$', 'provisioning.views.deleteuser'), 51 | ) 52 | -------------------------------------------------------------------------------- /overmind/provisioning/provider_meta.py: -------------------------------------------------------------------------------- 1 | # List of supported providers and related info 2 | from django.conf import settings 3 | from provisioning import plugins 4 | 5 | LIBCLOUD_PROVIDERS = { 6 | 'DUMMY': { 7 | 'display_name': 'Dummy Provider', 8 | 'access_key': 'Dummy Access Key', 9 | 'secret_key': None, 10 | }, 11 | 'EC2_US_WEST': { 12 | 'display_name': 'EC2 US West', 13 | 'access_key': 'AWS Access Key ID', 14 | 'secret_key': 'AWS Secret Key', 15 | # ex_keyname is needed for EC2 to have our ssh key deployed to nodes 16 | 'extra_param': ['ex_keyname', settings.PUBLIC_KEY_FILE.split(".")[0]], 17 | }, 18 | 'EC2_US_EAST': { 19 | 'display_name': 'EC2 US East', 20 | 'access_key': 'AWS Access Key ID', 21 | 'secret_key': 'AWS Secret Key', 22 | 'extra_param': ['ex_keyname', settings.PUBLIC_KEY_FILE.split(".")[0]], 23 | }, 24 | 'EC2_EU_WEST': { 25 | 'display_name': 'EC2 EU West', 26 | 'access_key': 'AWS Access Key ID', 27 | 'secret_key': 'AWS Secret Key', 28 | 'extra_param': ['ex_keyname', settings.PUBLIC_KEY_FILE.split(".")[0]], 29 | }, 30 | 'RACKSPACE': { 31 | 'display_name': 'Rackspace', 32 | 'access_key': 'Username', 33 | 'secret_key': 'API Access Key', 34 | }, 35 | } 36 | 37 | PROVIDERS = {} 38 | 39 | def add_libcloud_providers(): 40 | for provider in LIBCLOUD_PROVIDERS.keys(): 41 | PROVIDERS[provider] = LIBCLOUD_PROVIDERS[provider] 42 | PROVIDERS[provider]['supported_actions'] = [ 43 | 'create', 'destroy', 'reboot', 44 | 'list', 'images', 'sizes', 'locations', 45 | ] 46 | PROVIDERS[provider]['form_fields'] = ['image', 'size', 'location'] 47 | 48 | def add_plugins(): 49 | plugin_dict = plugins.load_plugins() 50 | for provider in plugin_dict.keys(): 51 | PROVIDERS[provider] = plugin_dict[provider] 52 | 53 | add_libcloud_providers() 54 | add_plugins() 55 | -------------------------------------------------------------------------------- /overmind/provisioning/management/commands/create_groups.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | from django.contrib.auth.models import User, Group, Permission 3 | 4 | class Command(BaseCommand): 5 | help = 'Creates predefined user Roles' 6 | 7 | def handle(self, *args, **options): 8 | auth_perms = ['add_user', 'change_user', 'delete_user'] 9 | provisioning_perms = [ 10 | 'add_provider', 'change_provider', 'delete_provider', 11 | 'add_node', 'change_node', 'delete_node', 12 | ] 13 | 14 | try: 15 | admin = Group.objects.get(name='Admin') 16 | except Group.DoesNotExist: 17 | admin = Group(name='Admin') 18 | admin.save() 19 | try: 20 | op = Group.objects.get(name='Operator') 21 | except Group.DoesNotExist: 22 | op = Group(name='Operator') 23 | op.save() 24 | 25 | admin.permissions = [ 26 | Permission.objects.get(codename=codename) for codename in auth_perms] 27 | 28 | for codename in provisioning_perms: 29 | admin.permissions.add(Permission.objects.get(codename=codename)) 30 | op.permissions.add(Permission.objects.get(codename=codename)) 31 | 32 | # Add an Observer role with no rights 33 | try: 34 | ob = Group.objects.get(name='Observer') 35 | except Group.DoesNotExist: 36 | ob = Group(name='Observer') 37 | ob.save() 38 | 39 | # Remove superuser status (if any exist) and add the user to the admin group 40 | superusers = User.objects.filter(is_superuser=True) 41 | for user in superusers: 42 | user.is_superuser = False 43 | user.save() 44 | user.groups = [admin] 45 | 46 | verbosity = int(options.get('verbosity', 1)) 47 | if verbosity >= 1: 48 | print('Successfully loaded permission groups') 49 | 50 | -------------------------------------------------------------------------------- /overmind/templates/overview.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}: Overview{% endblock %} 3 | {% block script %} 4 | 25 | {% endblock %} 26 | {% block content %} 27 |

Nodesupdate

28 |
29 |
    30 | {% for row in nodes %}
  • {{ row.node.provider }} - {{ row.node.name }} - {{ row.node.public_ips.0 }} - {{ row.node.state }}{% for a in row.actions %}{{ a.label }}{% endfor %}
  • 31 | {% endfor %} 32 |
33 | {% endblock %} 34 | {% block sidebar %} 35 | 43 | {% endblock %} -------------------------------------------------------------------------------- /overmind/provisioning/tasks.py: -------------------------------------------------------------------------------- 1 | from celery.task import task, periodic_task 2 | from celery.task.sets import subtask 3 | from libcloud.common.types import InvalidCredsException 4 | from provisioning.models import Provider 5 | from datetime import timedelta 6 | 7 | 8 | @periodic_task(run_every=timedelta(seconds=30)) 9 | def update_providers(**kwargs): 10 | logger = update_providers.get_logger(**kwargs) 11 | logger.debug("Syncing providers...") 12 | for prov in Provider.objects.filter(ready=True): 13 | import_sizes.delay(prov.id) 14 | import_nodes.delay(prov.id) 15 | 16 | @task() 17 | def import_provider_info(provider_id, **kwargs): 18 | logger = import_provider_info.get_logger(**kwargs) 19 | prov = Provider.objects.get(id=provider_id) 20 | logger.debug('Importing info for provider %s...' % prov) 21 | import_images.delay(provider_id, callback=subtask(import_locations, 22 | callback=subtask(import_sizes, 23 | callback=subtask(import_nodes)))) 24 | 25 | @task(ignore_result=True) 26 | def import_images(provider_id, callback=None, **kwargs): 27 | logger = import_images.get_logger(**kwargs) 28 | prov = Provider.objects.get(id=provider_id) 29 | logger.debug('Importing images for provider %s...' % prov) 30 | prov.import_images() 31 | if callback: 32 | subtask(callback).delay(provider_id) 33 | 34 | @task(ignore_result=True) 35 | def import_locations(provider_id, callback=None, **kwargs): 36 | logger = import_locations.get_logger(**kwargs) 37 | prov = Provider.objects.get(id=provider_id) 38 | logger.debug('Importing locations for provider %s...' % prov) 39 | prov.import_locations() 40 | if callback: 41 | subtask(callback).delay(provider_id) 42 | 43 | @task(ignore_result=True) 44 | def import_sizes(provider_id, callback=None, **kwargs): 45 | logger = import_sizes.get_logger(**kwargs) 46 | prov = Provider.objects.get(id=provider_id) 47 | logger.debug('Importing sizes for provider %s...' % prov) 48 | prov.import_sizes() 49 | if callback: 50 | subtask(callback).delay(provider_id) 51 | 52 | 53 | @task(ignore_result=True) 54 | def import_nodes(provider_id, **kwargs): 55 | logger = import_nodes.get_logger(**kwargs) 56 | prov = Provider.objects.get(id=provider_id) 57 | logger.debug('Importing nodes for provider %s...' % prov) 58 | prov.import_nodes() 59 | if not prov.ready: 60 | prov.ready = True 61 | prov.save() 62 | -------------------------------------------------------------------------------- /overmind/templates/settings.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}: Settings{% endblock %} 3 | {% block script %} 4 | 41 | {% endblock %} 42 | {% block content %} 43 |
44 | {% if perms.provisioning %} 45 | 46 | 47 | 48 | 49 | 50 | {% for u in user_list %} 51 | 52 | 53 | {% endfor %} 54 |
Namefirst_namelast_nameemailRole
{{ u.username }}{{ u.first_name }}{{ u.last_name }}{{ u.email }}{{ u.groups.all.0 }}
55 |
56 | {% if perms.auth %} 57 |

Create new user

58 |
59 | {% else %}

You don't have permissions to edit or create other users

60 |

Edit your profile

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 |

Launch a new node

97 |
    98 |
  • 99 |

    Provider

    100 | {% if not provider_list|length %}

    No providers configured yet{% endif %} 101 |

      {% for provider in provider_list %} 102 |
    • 103 | 104 | 105 |
    • {% endfor %} 106 |
    107 |
  • 108 |
109 |
110 |
111 |
112 | {% endblock %} 113 | 114 | -------------------------------------------------------------------------------- /overmind/media/js/jquery.tooltip.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Tooltip plugin 1.3 3 | * 4 | * http://bassistance.de/jquery-plugins/jquery-plugin-tooltip/ 5 | * http://docs.jquery.com/Plugins/Tooltip 6 | * 7 | * Copyright (c) 2006 - 2008 Jörn Zaefferer 8 | * 9 | * $Id: jquery.tooltip.js 5741 2008-06-21 15:22:16Z joern.zaefferer $ 10 | * 11 | * Dual licensed under the MIT and GPL licenses: 12 | * http://www.opensource.org/licenses/mit-license.php 13 | * http://www.gnu.org/licenses/gpl.html 14 | */;(function($){var helper={},current,title,tID,IE=$.browser.msie&&/MSIE\s(5\.5|6\.)/.test(navigator.userAgent),track=false;$.tooltip={blocked:false,defaults:{delay:200,fade:false,showURL:true,extraClass:"",top:15,left:15,id:"tooltip"},block:function(){$.tooltip.blocked=!$.tooltip.blocked;}};$.fn.extend({tooltip:function(settings){settings=$.extend({},$.tooltip.defaults,settings);createHelper(settings);return this.each(function(){$.data(this,"tooltip",settings);this.tOpacity=helper.parent.css("opacity");this.tooltipText=this.title;$(this).removeAttr("title");this.alt="";}).mouseover(save).mouseout(hide).click(hide);},fixPNG:IE?function(){return this.each(function(){var image=$(this).css('backgroundImage');if(image.match(/^url\(["']?(.*\.png)["']?\)$/i)){image=RegExp.$1;$(this).css({'backgroundImage':'none','filter':"progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=crop, src='"+image+"')"}).each(function(){var position=$(this).css('position');if(position!='absolute'&&position!='relative')$(this).css('position','relative');});}});}:function(){return this;},unfixPNG:IE?function(){return this.each(function(){$(this).css({'filter':'',backgroundImage:''});});}:function(){return this;},hideWhenEmpty:function(){return this.each(function(){$(this)[$(this).html()?"show":"hide"]();});},url:function(){return this.attr('href')||this.attr('src');}});function createHelper(settings){if(helper.parent)return;helper.parent=$('

').appendTo(document.body).hide();if($.fn.bgiframe)helper.parent.bgiframe();helper.title=$('h3',helper.parent);helper.body=$('div.body',helper.parent);helper.url=$('div.url',helper.parent);}function settings(element){return $.data(element,"tooltip");}function handle(event){if(settings(this).delay)tID=setTimeout(show,settings(this).delay);else 15 | show();track=!!settings(this).track;$(document.body).bind('mousemove',update);update(event);}function save(){if($.tooltip.blocked||this==current||(!this.tooltipText&&!settings(this).bodyHandler))return;current=this;title=this.tooltipText;if(settings(this).bodyHandler){helper.title.hide();var bodyContent=settings(this).bodyHandler.call(this);if(bodyContent.nodeType||bodyContent.jquery){helper.body.empty().append(bodyContent)}else{helper.body.html(bodyContent);}helper.body.show();}else if(settings(this).showBody){var parts=title.split(settings(this).showBody);helper.title.html(parts.shift()).show();helper.body.empty();for(var i=0,part;(part=parts[i]);i++){if(i>0)helper.body.append("
");helper.body.append(part);}helper.body.hideWhenEmpty();}else{helper.title.html(title).show();helper.body.hide();}if(settings(this).showURL&&$(this).url())helper.url.html($(this).url().replace('http://','')).show();else 16 | helper.url.hide();helper.parent.addClass(settings(this).extraClass);if(settings(this).fixPNG)helper.parent.fixPNG();handle.apply(this,arguments);}function show(){tID=null;if((!IE||!$.fn.bgiframe)&&settings(current).fade){if(helper.parent.is(":animated"))helper.parent.stop().show().fadeTo(settings(current).fade,current.tOpacity);else 17 | helper.parent.is(':visible')?helper.parent.fadeTo(settings(current).fade,current.tOpacity):helper.parent.fadeIn(settings(current).fade);}else{helper.parent.show();}update();}function update(event){if($.tooltip.blocked)return;if(event&&event.target.tagName=="OPTION"){return;}if(!track&&helper.parent.is(":visible")){$(document.body).unbind('mousemove',update)}if(current==null){$(document.body).unbind('mousemove',update);return;}helper.parent.removeClass("viewport-right").removeClass("viewport-bottom");var left=helper.parent[0].offsetLeft;var top=helper.parent[0].offsetTop;if(event){left=event.pageX+settings(current).left;top=event.pageY+settings(current).top;var right='auto';if(settings(current).positionLeft){right=$(window).width()-left;left='auto';}helper.parent.css({left:left,right:right,top:top});}var v=viewport(),h=helper.parent[0];if(v.x+v.cx;:/?']) 137 | 138 | return ''.join([random.choice(chars) for i in range(length)]) 139 | -------------------------------------------------------------------------------- /overmind/provisioning/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.models import User, Group 3 | from django.contrib.auth.forms import UserCreationForm 4 | from django.utils.safestring import mark_safe 5 | from django.utils.encoding import force_unicode 6 | 7 | from provisioning.models import Provider, Node, Image, Location, Size 8 | from provisioning.provider_meta import PROVIDERS 9 | 10 | class ProviderForm(forms.ModelForm): 11 | def __init__(self, provider_type, *args, **kwargs): 12 | super(ProviderForm, self).__init__(*args, **kwargs) 13 | self.fields['provider_type'].widget = forms.HiddenInput() 14 | self.fields['provider_type'].initial = provider_type 15 | provider_type_info = PROVIDERS.get(provider_type, {}) 16 | 17 | for field in ['access_key', 'secret_key']: 18 | label = provider_type_info.get(field) 19 | if label is None: 20 | self.fields[field].widget = forms.HiddenInput() 21 | else: 22 | self.fields[field].required = True 23 | self.fields[field].label = label 24 | if field == 'secret_key': 25 | self.fields['secret_key'].widget = forms.PasswordInput() 26 | 27 | class Meta: 28 | model = Provider 29 | fields = ('name', 'provider_type', 'access_key', 'secret_key') 30 | 31 | 32 | class AddImageForm(forms.Form): 33 | provider = forms.ModelChoiceField( 34 | queryset = Provider.objects.all(), 35 | widget = forms.HiddenInput, 36 | ) 37 | image_id = forms.CharField(widget=forms.HiddenInput, required=False) 38 | favimage1 = forms.CharField(label="Type an image id", required=False) 39 | favimage2 = forms.ChoiceField(label="or select an image", choices=[]) 40 | 41 | def __init__(self, provider_id, *args, **kwargs): 42 | super(AddImageForm, self).__init__(*args, **kwargs) 43 | prov = Provider.objects.get(id=provider_id) 44 | self.fields['provider'].initial = prov.id 45 | self.fields['favimage2'].choices = [] 46 | for img in prov.get_images().order_by('name'): 47 | self.fields['favimage2'].choices += [(img.id, img)] 48 | 49 | def clean(self): 50 | cleaned_data = self.cleaned_data 51 | image = cleaned_data.get('favimage1') 52 | if image != "": 53 | try: 54 | cleaned_data['image'] = Image.objects.get( 55 | provider=cleaned_data['provider'], 56 | image_id=image 57 | ) 58 | except Image.DoesNotExist: 59 | raise forms.ValidationError(u"Invalid image id") 60 | else: 61 | cleaned_data['image'] = Image.objects.get( 62 | id=cleaned_data.get('favimage2')) 63 | if cleaned_data['image'].favorite: 64 | msg = u"This image is already marked as favorite" 65 | self._errors['favimage1'] = self.error_class([msg]) 66 | return cleaned_data 67 | 68 | 69 | class CustomRadioFieldRenderer(forms.widgets.RadioFieldRenderer): 70 | def __init__(self, *args, **kwargs): 71 | super(CustomRadioFieldRenderer, self).__init__(*args, **kwargs) 72 | 73 | def render(self): 74 | """Outputs a
    for this set of radio fields.""" 75 | return mark_safe(u'
      \n%s\n
    ' % u'\n'.join([u'
  • %sx
  • ' 76 | % (force_unicode(w), w.choice_value) for w in self])) 77 | 78 | 79 | class SizeChoiceField(forms.ModelChoiceField): 80 | field_width = 10 81 | 82 | def __init__(self, width=None, *args, **kwargs): 83 | super(SizeChoiceField, self).__init__(*args, **kwargs) 84 | if width: 85 | self.field_width = width 86 | 87 | def label_from_instance(self, size): 88 | string = str(size) 89 | if size.price: 90 | blankspaces = self.field_width - len(str(size))- len(str(size.price)) 91 | if blankspaces < 0: 92 | blankspaces = 0 93 | string += ' '*blankspaces + size.price + ' $/hour' 94 | return mark_safe(string) 95 | 96 | 97 | class NodeForm(forms.ModelForm): 98 | provider = forms.ModelChoiceField( 99 | queryset = Provider.objects.all(), 100 | widget = forms.HiddenInput, 101 | ) 102 | 103 | location = forms.ModelChoiceField( 104 | queryset=None,widget=forms.HiddenInput,required=False) 105 | size = forms.ModelChoiceField( 106 | queryset=None,widget=forms.HiddenInput,required=False) 107 | image = forms.ModelChoiceField( 108 | queryset=None,widget=forms.HiddenInput,required=False) 109 | 110 | def __init__(self, provider_id, *args, **kwargs): 111 | super(NodeForm, self).__init__(*args, **kwargs) 112 | prov = Provider.objects.get(id=provider_id) 113 | self.fields['provider'].initial = prov.id 114 | provider_info = PROVIDERS[prov.provider_type] 115 | # Add custom plugin fields 116 | for field in provider_info.get('form_fields', []): 117 | # These fields will be added later 118 | if field in ['location', 'size', 'image']: 119 | continue 120 | self.fields[field] = forms.CharField(max_length=30) 121 | 122 | # Add location field 123 | if 'location' in provider_info.get('form_fields', []): 124 | locs = prov.get_locations() 125 | self.fields['location'] = forms.ModelChoiceField( 126 | queryset=locs, 127 | widget=forms.RadioSelect(), 128 | empty_label=None, 129 | ) 130 | if len(locs): 131 | self.fields['location'].initial = locs[0] 132 | 133 | # Add size field 134 | if 'size' in provider_info.get('form_fields', []): 135 | sizes = prov.get_sizes().order_by('price') 136 | width = None 137 | if len(sizes): 138 | width = max([len(str(s)) for s in sizes]) + 5 139 | 140 | self.fields['size'] = SizeChoiceField( 141 | queryset=sizes, 142 | width=width, 143 | empty_label=None, 144 | ) 145 | if len(sizes): 146 | self.fields['size'].initial = sizes[0] 147 | 148 | # Add image field 149 | if 'image' in provider_info.get('form_fields', []): 150 | images = prov.get_fav_images() 151 | self.fields['image'] = forms.ModelChoiceField( 152 | queryset=images, 153 | widget=forms.RadioSelect(renderer=CustomRadioFieldRenderer), 154 | empty_label=None, 155 | ) 156 | if len(images): 157 | self.fields['image'].initial = images[0] 158 | 159 | class Meta: 160 | model = Node 161 | fields = ('provider', 'name', 'location', 'size', 'image') 162 | 163 | class UserCreationFormExtended(UserCreationForm): 164 | def __init__(self, *args, **kwargs): 165 | super(UserCreationFormExtended, self).__init__(*args, **kwargs) 166 | self.fields['first_name'].required = True 167 | self.fields['username'].help_text = None 168 | self.fields['groups'] = forms.ModelChoiceField( 169 | queryset=Group.objects.all(), 170 | initial = 2,#id of group "Operator" 171 | help_text = None, 172 | required = True, 173 | label='Role', 174 | ) 175 | self.fields['password2'].help_text = None 176 | 177 | class Meta: 178 | model = User 179 | fields = ('username', 'email', 'first_name', 'last_name', 'groups') 180 | 181 | def save(self, commit=True): 182 | user = super(UserCreationFormExtended, self).save(commit=False) 183 | if commit: 184 | user.save() 185 | user.groups.add(self.cleaned_data["groups"]) 186 | user.save() 187 | return user 188 | 189 | class BasicEditForm(forms.ModelForm): 190 | password1 = forms.CharField( 191 | label="Password", widget=forms.PasswordInput, required=False) 192 | password2 = forms.CharField( 193 | label="Password confirmation", widget=forms.PasswordInput, required=False) 194 | 195 | def __init__(self, *args, **kwargs): 196 | super(BasicEditForm, self).__init__(*args, **kwargs) 197 | self.fields['first_name'].required = True 198 | 199 | def clean_password2(self): 200 | password1 = self.cleaned_data["password1"] 201 | password2 = self.cleaned_data["password2"] 202 | if password1 != password2: 203 | raise forms.ValidationError("The two password fields didn't match.") 204 | return password2 205 | 206 | def save(self, commit=True): 207 | user = super(BasicEditForm, self).save(commit=False) 208 | if self.cleaned_data["password1"] != "": 209 | user.set_password(self.cleaned_data["password1"]) 210 | 211 | if commit: 212 | user.save() 213 | 214 | return user 215 | 216 | class UserEditForm(BasicEditForm): 217 | group = forms.ModelChoiceField( 218 | queryset=Group.objects.all(), 219 | help_text=None, 220 | required=True, 221 | initial=2, 222 | label='Role', 223 | ) 224 | 225 | def __init__(self, *args, **kwargs): 226 | super(UserEditForm, self).__init__(*args, **kwargs) 227 | initial_group = kwargs.get('instance').groups.all() 228 | if len(initial_group): 229 | self.fields['group'].initial = initial_group[0].id 230 | 231 | class Meta: 232 | model = User 233 | fields = ('email', 'first_name', 'last_name', 'group') 234 | 235 | def save(self, commit=True): 236 | user = super(UserEditForm, self).save(commit=False) 237 | user.groups.clear() 238 | user.groups.add(self.cleaned_data["group"]) 239 | if commit: 240 | user.save() 241 | 242 | return user 243 | 244 | class ProfileEditForm(BasicEditForm): 245 | class Meta: 246 | model = User 247 | fields = ('email', 'first_name', 'last_name') 248 | -------------------------------------------------------------------------------- /overmind/api/provisioning.py: -------------------------------------------------------------------------------- 1 | from piston.handler import BaseHandler 2 | from piston.utils import rc 3 | from libcloud.common.types import InvalidCredsException 4 | 5 | from overmind.provisioning.provider_meta import PROVIDERS 6 | from overmind.provisioning.models import Provider, Image, Location, Size, Node 7 | from overmind.provisioning.models import get_state 8 | from overmind.provisioning.views import save_new_node, save_new_provider, update_provider 9 | import copy, logging 10 | 11 | # Unit tests are not working for HttpBasicAuthentication 12 | # This is a hack until authentication is reimplemented as OAuth 13 | # (waiting for a new piston version) 14 | _TESTING = False 15 | 16 | class ProviderHandler(BaseHandler): 17 | fields = ('id', 'name', 'provider_type', 'access_key') 18 | model = Provider 19 | 20 | def create(self, request): 21 | if not _TESTING and not request.user.has_perm('provisioning.add_provider'): 22 | return rc.FORBIDDEN 23 | 24 | if not hasattr(request, "data"): 25 | request.data = request.POST 26 | attrs = self.flatten_dict(request.data) 27 | 28 | # Pass data to form Validation 29 | error, form, provider = save_new_provider(attrs) 30 | if error is None: 31 | return provider 32 | else: 33 | resp = rc.BAD_REQUEST 34 | if error == 'form': 35 | for k, v in form.errors.items(): 36 | formerror = v[0] 37 | if type(formerror) != unicode: 38 | formerror = formerror.__unicode__() 39 | resp.write("\n" + k + ": " + formerror) 40 | else: 41 | resp.write("\n" + error) 42 | return resp 43 | 44 | def read(self, request, *args, **kwargs): 45 | id = kwargs.get('id') 46 | 47 | if id is None: 48 | provider_type = request.GET.get('provider_type') 49 | name = request.GET.get('name') 50 | if provider_type is not None: 51 | return self.model.objects.filter( 52 | provider_type=provider_type, 53 | ) 54 | elif name is not None: 55 | try: 56 | return self.model.objects.get(name=name) 57 | except self.model.DoesNotExist: 58 | return rc.NOT_FOUND 59 | else: 60 | return self.model.objects.all() 61 | else: 62 | try: 63 | return self.model.objects.get(id=id) 64 | except self.model.DoesNotExist: 65 | return rc.NOT_FOUND 66 | 67 | def update(self, request, *args, **kwargs): 68 | if not _TESTING and not request.user.has_perm('provisioning.change_provider'): 69 | return rc.FORBIDDEN 70 | if not hasattr(request, "data"): 71 | request.data = request.POST 72 | attrs = self.flatten_dict(request.data) 73 | 74 | # Check that it is a valid provider 75 | id = kwargs.get('id') 76 | if id is None: 77 | return rc.BAD_REQUEST 78 | 79 | try: 80 | provider = self.model.objects.get(id=id) 81 | except self.model.DoesNotExist: 82 | return rc.NOT_FOUND 83 | 84 | # Pass data to form Validation 85 | error, form, provider = update_provider(attrs, provider) 86 | if error is None: 87 | return provider 88 | else: 89 | resp = rc.BAD_REQUEST 90 | if error == 'form': 91 | for k, v in form.errors.items(): 92 | formerror = v[0] 93 | if type(formerror) != unicode: 94 | formerror = formerror.__unicode__() 95 | resp.write("\n" + k + ": " + formerror) 96 | else: 97 | resp.write("\n" + error) 98 | return resp 99 | 100 | def delete(self, request, *args, **kwargs): 101 | if not _TESTING and not request.user.has_perm('provisioning.delete_provider'): 102 | return rc.FORBIDDEN 103 | id = kwargs.get('id') 104 | if id is None: 105 | return rc.BAD_REQUEST 106 | try: 107 | prov = self.model.objects.get(id=id) 108 | prov.delete() 109 | return rc.DELETED 110 | except self.model.DoesNotExist: 111 | return rc.NOT_FOUND 112 | 113 | 114 | class ImageHandler(BaseHandler): 115 | fields = ('id', 'image_id', 'name', 'favorite') 116 | model = Image 117 | allowed_methods = ('GET',) 118 | 119 | def read(self, request, *args, **kwargs): 120 | id = kwargs.get('id') 121 | provider = Provider.objects.get(id=kwargs.get('provider_id')) 122 | if id is None: 123 | image_id = request.GET.get('image_id') 124 | name = request.GET.get('name') 125 | if image_id is not None: 126 | try: 127 | return self.model.objects.get( 128 | provider=provider, 129 | image_id=image_id 130 | ) 131 | except self.model.DoesNotExist: 132 | return rc.NOT_FOUND 133 | elif name is not None: 134 | try: 135 | return self.model.objects.get( 136 | provider=provider, 137 | name=name 138 | ) 139 | except self.model.DoesNotExist: 140 | return rc.NOT_FOUND 141 | else: 142 | return self.model.objects.filter(provider=provider) 143 | else: 144 | try: 145 | return self.model.objects.get(id=id) 146 | except self.model.DoesNotExist: 147 | return rc.NOT_FOUND 148 | 149 | 150 | class LocationHandler(BaseHandler): 151 | fields = ('id', 'location_id', 'name') 152 | model = Location 153 | allowed_methods = ('GET',) 154 | 155 | 156 | class SizeHandler(BaseHandler): 157 | fields = ('id', 'size_id', 'name') 158 | model = Size 159 | allowed_methods = ('GET',) 160 | 161 | 162 | class NodeHandler(BaseHandler): 163 | fields = ('id', 'name', 'node_id', 'provider', 'image', 'location', 'size', 164 | 'public_ips', 'private_ips', 'created_by', 'state', 'environment', 165 | 'destroyed_by', 'created_at', 'destroyed_at') 166 | model = Node 167 | 168 | def create(self, request): 169 | if not _TESTING and not request.user.has_perm('provisioning.add_node'): 170 | return rc.FORBIDDEN 171 | if not hasattr(request, "data"): 172 | request.data = request.POST 173 | attrs = self.flatten_dict(request.data) 174 | 175 | # Modify REST's "provider_id" to "provider" (expected form field) 176 | data = copy.deepcopy(attrs) 177 | data['provider'] = data.get('provider_id','') 178 | if 'provider_id' in data: del data['provider_id'] 179 | 180 | # Validate data and save new node 181 | error, form, node = save_new_node(data, request.user) 182 | if error is None: 183 | return node 184 | else: 185 | resp = rc.BAD_REQUEST 186 | if error == 'form': 187 | for k, v in form.errors.items(): 188 | formerror = v[0] 189 | if type(formerror) != unicode: 190 | formerror = formerror.__unicode__() 191 | resp.write("\n" + k + ": " + formerror) 192 | else: 193 | resp.write("\n" + error) 194 | return resp 195 | 196 | def read(self, request, *args, **kwargs): 197 | id = kwargs.get('id') 198 | 199 | if id is None: 200 | # If name specified, return node 201 | name = request.GET.get('name') 202 | if name is not None: 203 | try: 204 | return self.model.objects.get(name=name) 205 | except self.model.DoesNotExist: 206 | return rc.NOT_FOUND 207 | # Else return a subset of nodes 208 | query = self.model.objects.all() 209 | provider_id = request.GET.get('provider_id') 210 | if provider_id is not None: 211 | query = query.filter(provider=provider_id) 212 | if request.GET.get('show_decommissioned') != 'true': 213 | query = query.exclude(environment='Decommissioned') 214 | return query 215 | else: 216 | # Return the selected node 217 | try: 218 | return self.model.objects.get(id=id) 219 | except self.model.DoesNotExist: 220 | return rc.NOT_FOUND 221 | 222 | def update(self, request, *args, **kwargs): 223 | if not _TESTING and not request.user.has_perm('provisioning.change_node'): 224 | return rc.FORBIDDEN 225 | if not hasattr(request, "data"): 226 | request.data = request.POST 227 | attrs = self.flatten_dict(request.data) 228 | id = kwargs.get('id') 229 | if id is None: 230 | return rc.BAD_REQUEST 231 | 232 | try: 233 | node = self.model.objects.get(id=id) 234 | except self.model.DoesNotExist: 235 | return rc.NOT_FOUND 236 | 237 | # Update name if present 238 | name = attrs.get('name') 239 | if name is not None and name != node.name: 240 | try: 241 | self.model.objects.get(name=name) 242 | return rc.DUPLICATE_ENTRY 243 | except self.model.DoesNotExist: 244 | node.name = name 245 | node.save() 246 | return node 247 | 248 | def delete(self, request, *args, **kwargs): 249 | if not _TESTING and not request.user.has_perm('provisioning.delete_node'): 250 | return rc.FORBIDDEN 251 | id = kwargs.get('id') 252 | if id is None: 253 | return rc.BAD_REQUEST 254 | try: 255 | node = self.model.objects.get(id=id) 256 | if node.environment == 'Decommissioned': 257 | return rc.NOT_HERE 258 | if node.provider.supports('destroy'): 259 | node.destroy(request.user.username) 260 | else: 261 | node.decommission() 262 | return rc.DELETED 263 | except self.model.DoesNotExist: 264 | return rc.NOT_FOUND 265 | -------------------------------------------------------------------------------- /overmind/provisioning/views.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | 4 | from django.shortcuts import render_to_response, get_object_or_404 5 | from django.http import HttpResponse, HttpResponseRedirect 6 | from django.contrib.auth.models import User, Group 7 | from django.contrib.auth.decorators import login_required, permission_required 8 | from django.template import RequestContext 9 | from libcloud.common.types import InvalidCredsException 10 | 11 | from provisioning.models import Action, Provider, Node, get_state, Image 12 | from provisioning import tasks 13 | from provisioning.forms import ProviderForm, NodeForm, AddImageForm, ProfileEditForm 14 | from provisioning.forms import UserCreationFormExtended, UserEditForm 15 | from provisioning.provider_meta import PROVIDERS 16 | 17 | 18 | @login_required 19 | def overview(request): 20 | provider_list = Provider.objects.all() 21 | nodes = [] 22 | #TODO: Optimize for hundreds of nodes 23 | for n in Node.objects.exclude(environment='Decommissioned'): 24 | datatable = "" 25 | fields = [ 26 | ['Created by', n.created_by], 27 | ['Created at', n.created_at.strftime('%Y-%m-%d %H:%M:%S')], 28 | ['Node ID', n.node_id], 29 | ['OS image', n.image or "-"], 30 | ['Location', n.location or "-"], 31 | ['Size', n.size or "-"], 32 | ] 33 | if n.size and n.size.price: 34 | fields.append(['Price', n.size.price + ' $/hour']) 35 | fields.append(['-----', '-----']) 36 | if n.destroyed_by: 37 | fields.append(['Destroyed by', n.destroyed_by]) 38 | fields.append(['Destroyed at', n.destroyed_at]) 39 | if n.private_ip: 40 | fields.append(['private_ip', n.private_ip]) 41 | 42 | for key, val in n.extra_data().items(): 43 | fields.append([key, val]) 44 | 45 | for field in fields: 46 | datatable += "" 48 | datatable += "
    " + field[0] + ":" + str(field[1]) 47 | datatable += "
    " 49 | 50 | actions_list = [] 51 | if n.state != 'Terminated' and \ 52 | request.user.has_perm('provisioning.change_node'): 53 | actions = n.provider.actions.filter(show=True) 54 | 55 | if actions.filter(name='reboot'): 56 | actions_list.append({ 57 | 'action': 'reboot', 58 | 'label': 'reboot', 59 | 'confirmation': 'Are you sure you want to reboot the node "%s"'\ 60 | % n.name, 61 | }) 62 | 63 | if actions.filter(name='destroy'): 64 | actions_list.append({ 65 | 'action': 'destroy', 66 | 'label': 'destroy', 67 | 'confirmation': 'This action will completely destroy the node %s'\ 68 | % n.name, 69 | }) 70 | else: 71 | actions_list.append({ 72 | 'action': 'destroy', 73 | 'label': 'delete', 74 | 'confirmation': 'This action will remove the node %s with IP %s' % (n.name, n.public_ip), 75 | }) 76 | 77 | nodes.append({ 'node': n, 'data': datatable, 'actions': actions_list }) 78 | 79 | variables = RequestContext(request, { 80 | 'nodes': nodes, 81 | 'provider_list': provider_list, 82 | }) 83 | return render_to_response('overview.html', variables) 84 | 85 | @permission_required('provisioning.add_provider') 86 | def provider(request): 87 | providers = [] 88 | provider_types = PROVIDERS.keys() 89 | provider_types.sort() 90 | for p in provider_types: 91 | providers.append([p, PROVIDERS[p]['display_name']]) 92 | 93 | variables = RequestContext(request, { 94 | 'provider_types': providers, 'user': request.user, 95 | }) 96 | return render_to_response('provider.html', variables) 97 | 98 | @permission_required('provisioning.add_provider') 99 | def newprovider(request): 100 | error = None 101 | if request.method == 'POST': 102 | error, form, prov = save_new_provider(request.POST) 103 | if error is None: 104 | return HttpResponse('

    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. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------