2013-03-24 02:43:11 +04:00
# (c) 2013, AnsibleWorks, Michael DeHaan <michael@ansibleworks.com>
#
# This file is part of Ansible Commander
#
# Ansible Commander is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible Commander is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible Commander. If not, see <http://www.gnu.org/licenses/>.
2013-03-01 04:39:01 +04:00
from django . db import models
2013-03-14 00:29:51 +04:00
from django . db . models import CASCADE , SET_NULL , PROTECT
2013-03-14 01:57:25 +04:00
from django . utils . translation import ugettext_lazy as _
2013-03-21 08:12:03 +04:00
from django . core . urlresolvers import reverse
2013-03-24 00:21:23 +04:00
from django . contrib . auth . models import User
2013-03-22 22:48:18 +04:00
import exceptions
2013-03-01 04:39:01 +04:00
2013-03-14 00:29:51 +04:00
# TODO: jobs and events model TBD
# TODO: reporting model TBD
2013-03-13 21:09:36 +04:00
2013-03-22 21:41:35 +04:00
JOB_TYPE_CHOICES = [
( ' run ' , _ ( ' Run ' ) ) ,
( ' check ' , _ ( ' Check ' ) ) ,
]
2013-03-24 20:36:42 +04:00
class EditHelper ( object ) :
@classmethod
def illegal_changes ( cls , request , obj , model_class ) :
''' have any illegal changes been made (for a PUT request)? '''
can_admin = model_class . can_user_administrate ( request . user , obj )
if ( not can_admin ) or ( can_admin == ' partial ' ) :
check_fields = model_class . admin_only_edit_fields
changed = cls . fields_changed ( check_fields , obj , request . DATA )
if len ( changed . keys ( ) ) > 0 :
return True
return False
@classmethod
def fields_changed ( cls , fields , obj , data ) :
''' return the fields that would be changed by a prospective PUT operation '''
changed = { }
for f in fields :
left = getattr ( obj , f , None )
if left is None :
raise Exception ( " internal error, %s is not a member of %s " % ( f , obj ) )
right = data . get ( f , None )
if ( right is not None ) and ( left != right ) :
changed [ f ] = ( left , right )
return changed
class UserHelper ( object ) :
# fields that the user themselves cannot edit, but are not actually read only
admin_only_edit_fields = ( ' last_name ' , ' first_name ' , ' username ' , ' is_active ' , ' is_superuser ' )
@classmethod
def can_user_administrate ( cls , user , obj ) :
''' a user can be administrated if they are themselves, or by org admins or superusers '''
if user == obj :
return ' partial '
if user . is_superuser :
return True
matching_orgs = len ( set ( obj . organizations . all ( ) ) & set ( user . admin_of_organizations . all ( ) ) )
return matching_orgs
@classmethod
def can_user_read ( cls , user , obj ) :
''' a user can be read if they are on the same team or can be administrated '''
matching_teams = user . teams . filter ( users__in = [ user ] ) . count ( )
return matching_teams or cls . can_user_administrate ( user , obj )
2013-03-01 04:39:01 +04:00
class CommonModel ( models . Model ) :
2013-03-13 21:09:36 +04:00
'''
common model for all object types that have these standard fields
'''
2013-03-01 04:39:01 +04:00
class Meta :
abstract = True
2013-03-18 23:49:40 +04:00
name = models . CharField ( max_length = 512 , unique = True )
2013-03-22 21:41:35 +04:00
description = models . TextField ( blank = True , default = ' ' )
2013-03-22 22:23:50 +04:00
created_by = models . ForeignKey ( ' auth.User ' , on_delete = SET_NULL , null = True , related_name = ' %s (class)s_created ' ) # not blank=False on purpose for admin!
2013-03-16 01:53:44 +04:00
creation_date = models . DateField ( auto_now_add = True )
2013-03-24 00:03:17 +04:00
tags = models . ManyToManyField ( ' Tag ' , related_name = ' %(class)s _by_tag ' , blank = True )
audit_trail = models . ManyToManyField ( ' AuditTrail ' , related_name = ' %(class)s _by_audit_trail ' , blank = True )
2013-03-14 00:29:51 +04:00
active = models . BooleanField ( default = True )
2013-03-15 19:18:18 +04:00
def __unicode__ ( self ) :
return unicode ( self . name )
2013-03-22 22:48:18 +04:00
2013-03-22 23:53:08 +04:00
@classmethod
2013-03-23 00:52:44 +04:00
def can_user_administrate ( cls , user , obj ) :
2013-03-23 02:16:40 +04:00
# FIXME: do we want a seperate method to override put? This is kind of general purpose
2013-03-22 22:48:18 +04:00
raise exceptions . NotImplementedError ( )
2013-03-22 23:36:59 +04:00
2013-03-22 23:53:08 +04:00
@classmethod
2013-03-22 23:36:59 +04:00
def can_user_delete ( cls , user , obj ) :
2013-03-23 02:16:40 +04:00
raise exceptions . NotImplementedError ( )
2013-03-22 23:44:32 +04:00
2013-03-22 23:53:08 +04:00
@classmethod
2013-03-22 23:57:24 +04:00
def can_user_read ( cls , user , obj ) :
2013-03-22 23:44:32 +04:00
raise exceptions . NotImplementedError ( )
2013-03-22 23:36:59 +04:00
2013-03-23 22:31:36 +04:00
@classmethod
def can_user_add ( cls , user ) :
return user . is_superuser
2013-03-23 00:52:44 +04:00
@classmethod
2013-03-24 00:34:52 +04:00
def can_user_attach ( cls , user , obj , sub_obj , relationship_type ) :
2013-03-23 02:16:40 +04:00
''' whether you can add sub_obj to obj using the relationship type in a subobject view '''
2013-03-24 00:34:52 +04:00
if type ( sub_obj ) != User :
if not sub_obj . can_user_read ( user , sub_obj ) :
return False
rc = cls . can_user_administrate ( user , obj )
return rc
2013-03-23 00:52:44 +04:00
@classmethod
2013-03-23 22:31:36 +04:00
def can_user_unattach ( cls , user , obj , sub_obj , relationship ) :
2013-03-23 02:07:06 +04:00
return cls . can_user_administrate ( user , obj )
2013-03-13 23:15:35 +04:00
class Tag ( models . Model ) :
'''
any type of object can be given a search tag
'''
2013-03-14 00:29:51 +04:00
class Meta :
app_label = ' main '
2013-03-13 21:09:36 +04:00
2013-03-15 19:45:14 +04:00
name = models . CharField ( max_length = 512 )
2013-03-15 19:18:18 +04:00
def __unicode__ ( self ) :
return unicode ( self . name )
2013-03-24 00:03:17 +04:00
def get_absolute_url ( self ) :
import lib . urls
return reverse ( lib . urls . views_TagsDetail , args = ( self . pk , ) )
2013-03-24 00:34:52 +04:00
@classmethod
def can_user_add ( cls , user ) :
# anybody can make up tags
return True
@classmethod
def can_user_read ( cls , user , obj ) :
# anybody can read tags, we won't show much detail other than the names
return True
2013-03-13 21:09:36 +04:00
2013-03-24 01:07:24 +04:00
class AuditTrail ( models . Model ) :
2013-03-13 21:09:36 +04:00
'''
changing any object records the change
'''
2013-03-14 00:29:51 +04:00
class Meta :
app_label = ' main '
2013-03-13 21:09:36 +04:00
2013-03-15 19:45:14 +04:00
resource_type = models . CharField ( max_length = 64 )
2013-03-22 19:35:26 +04:00
modified_by = models . ForeignKey ( ' auth.User ' , on_delete = SET_NULL , null = True , blank = True )
2013-03-13 23:15:35 +04:00
delta = models . TextField ( ) # FIXME: switch to JSONField
2013-03-13 21:09:36 +04:00
detail = models . TextField ( )
comment = models . TextField ( )
2013-03-21 23:28:40 +04:00
# FIXME: this looks like this should be a ManyToMany
2013-03-15 19:18:18 +04:00
tag = models . ForeignKey ( ' Tag ' , on_delete = SET_NULL , null = True , blank = True )
2013-03-01 04:39:01 +04:00
2013-03-13 21:09:36 +04:00
class Organization ( CommonModel ) :
'''
organizations are the basic unit of multi - tenancy divisions
'''
2013-03-14 00:29:51 +04:00
class Meta :
app_label = ' main '
2013-03-13 21:09:36 +04:00
2013-03-22 19:35:26 +04:00
users = models . ManyToManyField ( ' auth.User ' , blank = True , related_name = ' organizations ' )
admins = models . ManyToManyField ( ' auth.User ' , blank = True , related_name = ' admin_of_organizations ' )
2013-03-15 19:18:18 +04:00
projects = models . ManyToManyField ( ' Project ' , blank = True , related_name = ' organizations ' )
2013-03-13 21:09:36 +04:00
2013-03-21 08:12:03 +04:00
def get_absolute_url ( self ) :
import lib . urls
return reverse ( lib . urls . views_OrganizationsDetail , args = ( self . pk , ) )
2013-03-22 23:53:08 +04:00
@classmethod
2013-03-22 23:36:59 +04:00
def can_user_delete ( cls , user , obj ) :
return user in obj . admins . all ( )
2013-03-22 23:53:08 +04:00
@classmethod
2013-03-22 23:44:32 +04:00
def can_user_administrate ( cls , user , obj ) :
2013-03-23 02:16:40 +04:00
# FIXME: super user checks should be higher up so we don't have to repeat them
2013-03-23 00:52:44 +04:00
if user . is_superuser :
return True
2013-03-23 23:08:02 +04:00
if obj . created_by == user :
return True
2013-03-23 00:52:44 +04:00
rc = user in obj . admins . all ( )
return rc
2013-03-22 23:44:32 +04:00
2013-03-22 23:53:08 +04:00
@classmethod
2013-03-22 23:57:24 +04:00
def can_user_read ( cls , user , obj ) :
2013-03-23 23:08:02 +04:00
return cls . can_user_administrate ( user , obj ) or user in obj . users . all ( )
2013-03-23 00:52:44 +04:00
2013-03-22 23:53:08 +04:00
@classmethod
2013-03-22 23:44:32 +04:00
def can_user_delete ( cls , user , obj ) :
2013-03-22 23:53:08 +04:00
return cls . can_user_administrate ( user , obj )
2013-03-22 23:44:32 +04:00
2013-03-23 02:55:10 +04:00
def __unicode__ ( self ) :
return self . name
2013-03-13 21:09:36 +04:00
class Inventory ( CommonModel ) :
'''
an inventory source contains lists and hosts .
'''
2013-03-14 00:29:51 +04:00
class Meta :
app_label = ' main '
2013-03-14 01:57:25 +04:00
verbose_name_plural = _ ( ' inventories ' )
2013-03-13 21:09:36 +04:00
2013-03-14 00:29:51 +04:00
organization = models . ForeignKey ( Organization , null = True , on_delete = SET_NULL , related_name = ' inventories ' )
2013-03-13 21:09:36 +04:00
2013-03-23 02:55:10 +04:00
def __unicode__ ( self ) :
if self . organization :
return u ' %s ( %s ) ' % ( self . name , self . organization )
else :
return self . name
2013-03-13 21:09:36 +04:00
class Host ( CommonModel ) :
'''
A managed node
'''
2013-03-14 00:29:51 +04:00
class Meta :
app_label = ' main '
2013-03-13 21:09:36 +04:00
2013-03-14 00:29:51 +04:00
inventory = models . ForeignKey ( ' Inventory ' , null = True , on_delete = SET_NULL , related_name = ' hosts ' )
2013-03-13 21:09:36 +04:00
2013-03-23 02:55:10 +04:00
def __unicode__ ( self ) :
return self . name
2013-03-13 21:09:36 +04:00
class Group ( CommonModel ) :
'''
A group of managed nodes . May belong to multiple groups
'''
2013-03-14 00:29:51 +04:00
class Meta :
app_label = ' main '
2013-03-13 21:09:36 +04:00
2013-03-14 00:29:51 +04:00
inventory = models . ForeignKey ( ' Inventory ' , null = True , on_delete = SET_NULL , related_name = ' groups ' )
2013-03-23 02:55:10 +04:00
parents = models . ManyToManyField ( ' self ' , symmetrical = False , related_name = ' children ' , blank = True )
2013-03-15 19:26:32 +04:00
hosts = models . ManyToManyField ( ' Host ' , related_name = ' groups ' , blank = True )
2013-03-13 21:09:36 +04:00
2013-03-23 02:55:10 +04:00
def __unicode__ ( self ) :
return self . name
2013-03-13 23:15:35 +04:00
# FIXME: audit nullables
# FIXME: audit cascades
2013-03-13 21:09:36 +04:00
2013-03-13 23:15:35 +04:00
class VariableData ( CommonModel ) :
2013-03-13 21:09:36 +04:00
'''
2013-03-13 23:15:35 +04:00
A set of host or group variables
2013-03-13 21:09:36 +04:00
'''
2013-03-14 00:29:51 +04:00
class Meta :
app_label = ' main '
2013-03-14 01:57:25 +04:00
verbose_name_plural = _ ( ' variable data ' )
2013-03-13 21:09:36 +04:00
2013-03-14 00:29:51 +04:00
host = models . ForeignKey ( ' Host ' , null = True , default = None , blank = True , on_delete = CASCADE , related_name = ' variable_data ' )
group = models . ForeignKey ( ' Group ' , null = True , default = None , blank = True , on_delete = CASCADE , related_name = ' variable_data ' )
2013-03-13 23:15:35 +04:00
data = models . TextField ( ) # FIXME: JsonField
2013-03-13 21:09:36 +04:00
2013-03-23 02:55:10 +04:00
def __unicode__ ( self ) :
return ' %s = %s ' % ( self . name , self . data )
2013-03-13 21:09:36 +04:00
class Credential ( CommonModel ) :
'''
A credential contains information about how to talk to a remote set of hosts
Usually this is a SSH key location , and possibly an unlock password .
If used with sudo , a sudo password should be set if required .
'''
2013-03-14 00:29:51 +04:00
class Meta :
app_label = ' main '
2013-03-13 21:09:36 +04:00
2013-03-22 19:35:26 +04:00
user = models . ForeignKey ( ' auth.User ' , null = True , default = None , blank = True , on_delete = SET_NULL , related_name = ' credentials ' )
2013-03-14 00:29:51 +04:00
project = models . ForeignKey ( ' Project ' , null = True , default = None , blank = True , on_delete = SET_NULL , related_name = ' credentials ' )
team = models . ForeignKey ( ' Team ' , null = True , default = None , blank = True , on_delete = SET_NULL , related_name = ' credentials ' )
2013-03-13 23:15:35 +04:00
2013-03-15 19:45:14 +04:00
ssh_key_path = models . CharField ( blank = True , default = ' ' , max_length = 4096 )
2013-03-13 23:15:35 +04:00
ssh_key_data = models . TextField ( blank = True , default = ' ' ) # later
2013-03-15 19:45:14 +04:00
ssh_key_unlock = models . CharField ( blank = True , default = ' ' , max_length = 1024 )
ssh_password = models . CharField ( blank = True , default = ' ' , max_length = 1024 )
sudo_password = models . CharField ( blank = True , default = ' ' , max_length = 1024 )
2013-03-13 21:09:36 +04:00
class Team ( CommonModel ) :
'''
A team is a group of users that work on common projects .
'''
2013-03-14 00:29:51 +04:00
class Meta :
app_label = ' main '
2013-03-15 19:18:18 +04:00
projects = models . ManyToManyField ( ' Project ' , blank = True , related_name = ' teams ' )
2013-03-22 19:35:26 +04:00
users = models . ManyToManyField ( ' auth.User ' , blank = True , related_name = ' teams ' )
2013-03-13 23:15:35 +04:00
organization = models . ManyToManyField ( ' Organization ' , related_name = ' teams ' )
2013-03-13 21:09:36 +04:00
class Project ( CommonModel ) :
'''
A project represents a playbook git repo that can access a set of inventories
'''
2013-03-14 00:29:51 +04:00
2013-03-15 19:18:18 +04:00
inventories = models . ManyToManyField ( ' Inventory ' , blank = True , related_name = ' projects ' )
2013-03-15 19:45:14 +04:00
local_repository = models . CharField ( max_length = 1024 )
scm_type = models . CharField ( max_length = 64 )
default_playbook = models . CharField ( max_length = 1024 )
2013-03-13 21:09:36 +04:00
2013-03-22 01:38:53 +04:00
def get_absolute_url ( self ) :
import lib . urls
return reverse ( lib . urls . views_ProjectsDetail , args = ( self . pk , ) )
2013-03-22 23:53:08 +04:00
@classmethod
def can_user_administrate ( cls , user , obj ) :
2013-03-23 00:52:44 +04:00
if user . is_superuser :
return True
2013-03-23 23:08:02 +04:00
if obj . created_by == user :
return True
2013-03-23 00:52:44 +04:00
organizations = Organization . objects . filter ( admins__in = [ user ] , projects__in = [ obj ] )
2013-03-22 22:48:18 +04:00
for org in organizations :
if org in project . organizations ( ) :
return True
2013-03-23 00:52:44 +04:00
return False
@classmethod
def can_user_read ( cls , user , obj ) :
if cls . can_user_administrate ( user , obj ) :
return True
# and also if I happen to be on a team inside the project
# FIXME: add this too
return False
2013-03-22 22:48:18 +04:00
2013-03-13 21:09:36 +04:00
class Permission ( CommonModel ) :
'''
A permission allows a user , project , or team to be able to use an inventory source .
'''
2013-03-14 00:29:51 +04:00
class Meta :
app_label = ' main '
2013-03-13 21:09:36 +04:00
2013-03-22 19:35:26 +04:00
user = models . ForeignKey ( ' auth.User ' , null = True , on_delete = SET_NULL , blank = True , related_name = ' permissions ' )
2013-03-15 19:18:18 +04:00
project = models . ForeignKey ( ' Project ' , null = True , on_delete = SET_NULL , blank = True , related_name = ' permissions ' )
team = models . ForeignKey ( ' Team ' , null = True , on_delete = SET_NULL , blank = True , related_name = ' permissions ' )
2013-03-22 21:41:35 +04:00
job_type = models . CharField ( max_length = 64 , choices = JOB_TYPE_CHOICES )
2013-03-13 21:09:36 +04:00
2013-03-13 23:15:35 +04:00
# TODO: other job types (later)
2013-03-13 21:09:36 +04:00
class LaunchJob ( CommonModel ) :
2013-03-13 23:15:35 +04:00
'''
a launch job is a request to apply a project to an inventory source with a given credential
'''
2013-03-14 00:29:51 +04:00
class Meta :
app_label = ' main '
2013-03-13 21:09:36 +04:00
2013-03-14 00:29:51 +04:00
inventory = models . ForeignKey ( ' Inventory ' , on_delete = SET_NULL , null = True , default = None , blank = True , related_name = ' launch_jobs ' )
credential = models . ForeignKey ( ' Credential ' , on_delete = SET_NULL , null = True , default = None , blank = True , related_name = ' launch_jobs ' )
project = models . ForeignKey ( ' Project ' , on_delete = SET_NULL , null = True , default = None , blank = True , related_name = ' launch_jobs ' )
2013-03-22 19:35:26 +04:00
user = models . ForeignKey ( ' auth.User ' , on_delete = SET_NULL , null = True , default = None , blank = True , related_name = ' launch_jobs ' )
2013-03-22 21:41:35 +04:00
job_type = models . CharField ( max_length = 64 , choices = JOB_TYPE_CHOICES )
2013-03-22 19:49:04 +04:00
2013-03-23 02:55:10 +04:00
def start ( self ) :
from lib . main . tasks import run_launch_job
return run_launch_job . delay ( self . pk )
2013-03-22 19:49:04 +04:00
# project has one default playbook but really should have a list of playbooks and flags ...
2013-03-13 23:15:35 +04:00
2013-03-22 19:49:04 +04:00
# ENOUGH_TO_RUN_DJANGO=foo ACOM_INVENTORY_ID=<pk> ansible-playbook <path to project selected playbook.yml> -i ansible-commander-inventory.py
# ^-- this is a hard coded path
# ssh-agent bash
# ssh-add ... < key entry
#
# inventory script I can write, and will use ACOM_INVENTORY_ID
#
#
# playbook in source control is already on the disk
# job_type:
# run, check -- enough for now, more initially
# if check, add "--check" to parameters
2013-03-13 21:09:36 +04:00
2013-03-22 20:00:11 +04:00
# we'll extend ansible core to have callback context like
# self.context.playbook
# self.context.runner
# and the callback will read the environment for ACOM_CELERY_JOB_ID or similar
# and log tons into the database
# we'll also log stdout/stderr somewhere for debugging
# the ansible commander setup instructions will include installing the database logging callback
# inventory script is going to need some way to load Django models
# it is documented on ansible.cc under API docs and takes two parameters
# --list
# -- host <hostname>
# posting the LaunchJob should return some type of resource that we can check for status
# that all the log data will use as a Foreign Key
2013-03-13 21:09:36 +04:00
# TODO: Events
2013-03-01 04:39:01 +04:00
2013-03-13 23:15:35 +04:00
class LaunchJobStatus ( CommonModel ) :
2013-03-14 00:29:51 +04:00
class Meta :
app_label = ' main '
2013-03-14 01:57:25 +04:00
verbose_name_plural = _ ( ' launch job statuses ' )
2013-03-13 23:15:35 +04:00
2013-03-14 00:29:51 +04:00
launch_job = models . ForeignKey ( ' LaunchJob ' , null = True , on_delete = SET_NULL , related_name = ' launch_job_statuses ' )
2013-03-13 23:15:35 +04:00
status = models . IntegerField ( )
result_data = models . TextField ( )
# TODO: reporting (MPD)
2013-03-01 04:39:01 +04:00