2013-04-02 00:04:27 +04:00
# Copyright (c) 2013 AnsibleWorks, Inc.
2013-03-24 02:43:11 +04:00
#
2013-04-09 09:05:55 +04:00
# This file is part of Ansible Commander.
#
2013-03-24 02:43:11 +04:00
# Ansible Commander is free software: you can redistribute it and/or modify
2013-04-09 09:05:55 +04:00
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
2013-03-24 02:43:11 +04:00
#
2013-04-09 09:05:55 +04:00
# Ansible Commander is distributed in the hope that it will be useful,
2013-03-24 02:43:11 +04:00
# but WITHOUT ANY WARRANTY; without even the implied warranty of
2013-04-09 09:05:55 +04:00
# 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-29 10:36:11 +04:00
2013-04-10 08:41:51 +04:00
from django . db import models , DatabaseError
2013-03-14 00:29:51 +04:00
from django . db . models import CASCADE , SET_NULL , PROTECT
2013-04-10 08:41:51 +04:00
from django . db . models . signals import post_save
from django . dispatch import receiver
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-29 10:36:11 +04:00
from django . utils . timezone import now
2013-03-22 22:48:18 +04:00
import exceptions
2013-03-29 09:02:07 +04:00
from jsonfield import JSONField
from djcelery . models import TaskMeta
2013-04-10 08:41:51 +04:00
from rest_framework . authtoken . models import Token
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-26 01:21:17 +04:00
PERM_INVENTORY_ADMIN = ' admin '
2013-03-26 00:41:21 +04:00
PERM_INVENTORY_READ = ' read '
PERM_INVENTORY_WRITE = ' write '
PERM_INVENTORY_DEPLOY = ' run '
PERM_INVENTORY_CHECK = ' check '
2013-03-22 21:41:35 +04:00
JOB_TYPE_CHOICES = [
2013-03-26 00:41:21 +04:00
( PERM_INVENTORY_DEPLOY , _ ( ' Run ' ) ) ,
( PERM_INVENTORY_CHECK , _ ( ' Check ' ) ) ,
]
PERMISSION_TYPES = [
2013-03-26 01:21:17 +04:00
PERM_INVENTORY_ADMIN ,
2013-03-26 00:41:21 +04:00
PERM_INVENTORY_READ ,
PERM_INVENTORY_WRITE ,
PERM_INVENTORY_DEPLOY ,
PERM_INVENTORY_CHECK ,
]
2013-03-26 01:21:17 +04:00
PERMISSION_TYPES_ALLOWING_INVENTORY_READ = [
PERM_INVENTORY_ADMIN ,
PERM_INVENTORY_WRITE ,
PERM_INVENTORY_READ ,
]
PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE = [
PERM_INVENTORY_ADMIN ,
PERM_INVENTORY_WRITE ,
]
2013-03-26 00:41:21 +04:00
2013-03-26 01:21:17 +04:00
PERMISSION_TYPES_ALLOWING_INVENTORY_ADMIN = [
PERM_INVENTORY_ADMIN ,
]
# FIXME: TODO: make sure all of these are used and consistent
2013-03-26 00:41:21 +04:00
PERMISSION_TYPE_CHOICES = [
( PERM_INVENTORY_READ , _ ( ' Read Inventory ' ) ) ,
2013-03-26 01:21:17 +04:00
( PERM_INVENTORY_WRITE , _ ( ' Edit Inventory ' ) ) ,
( PERM_INVENTORY_ADMIN , _ ( ' Administrate Inventory ' ) ) ,
2013-03-26 00:41:21 +04:00
( PERM_INVENTORY_DEPLOY , _ ( ' Deploy To Inventory ' ) ) ,
( PERM_INVENTORY_CHECK , _ ( ' Deploy To Inventory (Dry Run) ' ) ) ,
2013-03-22 21:41:35 +04:00
]
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 )
2013-03-24 23:54:57 +04:00
if ( not can_admin ) or ( can_admin == ' partial ' ) :
2013-03-24 20:36:42 +04:00
check_fields = model_class . admin_only_edit_fields
changed = cls . fields_changed ( check_fields , obj , request . DATA )
2013-03-24 23:54:57 +04:00
if len ( changed . keys ( ) ) > 0 :
2013-03-24 20:36:42 +04:00
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 :
2013-03-24 23:54:57 +04:00
left = getattr ( obj , f , None )
2013-03-24 20:36:42 +04:00
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
2013-04-02 02:49:32 +04:00
matching_orgs = obj . organizations . filter ( admins__in = [ user ] ) . count ( )
2013-03-24 20:36:42 +04:00
return matching_orgs
@classmethod
def can_user_read ( cls , user , obj ) :
2013-03-24 23:54:57 +04:00
''' a user can be read if they are on the same team or can be administrated '''
2013-03-24 20:36:42 +04:00
matching_teams = user . teams . filter ( users__in = [ user ] ) . count ( )
return matching_teams or cls . can_user_administrate ( user , obj )
2013-03-24 23:54:57 +04:00
@classmethod
2013-03-24 22:14:59 +04:00
def can_user_delete ( cls , user , obj ) :
if user . is_superuser :
return True
2013-04-02 02:49:32 +04:00
matching_orgs = obj . organizations . filter ( admins__in = [ user ] ) . count ( )
2013-03-24 22:14:59 +04:00
return matching_orgs
2013-03-24 20:36:42 +04:00
2013-04-02 22:59:58 +04:00
@classmethod
def can_user_attach ( cls , user , obj , sub_obj , relationship_type ) :
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-24 20:36:42 +04:00
2013-04-16 03:19:54 +04:00
@classmethod
def can_user_unattach ( cls , user , obj , sub_obj , relationship_type ) :
return cls . can_user_administrate ( user , obj )
2013-03-26 22:44:12 +04:00
class PrimordialModel ( models . Model ) :
2013-03-24 23:54:57 +04:00
'''
common model for all object types that have these standard fields
2013-03-26 22:44:12 +04:00
must use a subclass CommonModel or CommonModelNameNotUnique though
as this lacks a name field .
2013-03-13 21:09:36 +04:00
'''
2013-03-01 04:39:01 +04:00
class Meta :
abstract = 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 23:54:57 +04:00
tags = models . ManyToManyField ( ' Tag ' , related_name = ' %(class)s _by_tag ' , blank = True )
2013-03-24 00:03:17 +04:00
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 ) :
2013-03-27 00:57:08 +04:00
return unicode ( " %s - %s " % ( self . name , self . id ) )
2013-03-22 22:48:18 +04:00
2013-03-24 23:54:57 +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-24 23:54:57 +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-24 23:54:57 +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
2013-03-26 03:00:07 +04:00
def can_user_add ( cls , user , data ) :
2013-03-23 22:31:36 +04:00
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-24 23:54:57 +04:00
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-24 23:54:57 +04:00
2013-03-26 22:44:12 +04:00
class CommonModel ( PrimordialModel ) :
''' a base model where the name is unique '''
class Meta :
abstract = True
name = models . CharField ( max_length = 512 , unique = True )
class CommonModelNameNotUnique ( PrimordialModel ) :
2013-04-02 22:59:58 +04:00
''' a base model where the name is not unique '''
2013-03-26 22:44:12 +04:00
class Meta :
abstract = True
2013-04-02 22:59:58 +04:00
2013-03-26 22:44:12 +04:00
name = models . CharField ( max_length = 512 , unique = False )
2013-03-13 23:15:35 +04:00
class Tag ( models . Model ) :
'''
2013-03-24 23:54:57 +04:00
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
2013-03-26 03:00:07 +04:00
def can_user_add ( cls , user , data ) :
2013-03-24 00:34:52 +04:00
# 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-24 23:54:57 +04:00
2013-03-24 01:07:24 +04:00
class AuditTrail ( models . Model ) :
2013-03-13 21:09:36 +04:00
'''
2013-03-24 23:54:57 +04:00
changing any object records the change
'''
2013-03-14 00:29:51 +04:00
class Meta :
app_label = ' main '
2013-03-24 23:54:57 +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 ) :
2013-03-24 23:54:57 +04:00
'''
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-24 23:54:57 +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-24 23:54:57 +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-24 23:54:57 +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-24 23:54:57 +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 ) :
2013-03-24 23:54:57 +04:00
'''
2013-03-13 21:09:36 +04:00
an inventory source contains lists and hosts .
'''
2013-03-24 23:54:57 +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 = _ ( ' inventories ' )
2013-03-27 00:57:08 +04:00
unique_together = ( ( " name " , " organization " ) , )
2013-03-24 23:54:57 +04:00
2013-03-26 22:44:12 +04:00
organization = models . ForeignKey ( Organization , null = False , related_name = ' inventories ' )
2013-04-02 22:59:58 +04:00
2013-03-26 00:41:21 +04:00
def get_absolute_url ( self ) :
import lib . urls
return reverse ( lib . urls . views_InventoryDetail , args = ( self . pk , ) )
2013-03-13 21:09:36 +04:00
2013-03-26 01:21:17 +04:00
@classmethod
def _has_permission_types ( cls , user , obj , allowed ) :
if user . is_superuser :
return True
2013-03-26 03:00:07 +04:00
by_org_admin = obj . organization . admins . filter ( pk = user . pk ) . count ( )
2013-03-26 01:21:17 +04:00
by_team_permission = obj . permissions . filter (
team__in = user . teams . all ( ) ,
permission_type__in = allowed
) . count ( )
by_user_permission = obj . permissions . filter (
user = user ,
permission_type__in = allowed
) . count ( )
2013-04-02 22:59:58 +04:00
2013-03-26 03:00:07 +04:00
result = ( by_org_admin + by_team_permission + by_user_permission )
return result > 0
2013-03-26 22:44:12 +04:00
@classmethod
def _has_any_inventory_permission_types ( cls , user , allowed ) :
2013-04-02 22:59:58 +04:00
'''
rather than checking for a permission on a specific inventory , return whether we have
2013-03-26 22:44:12 +04:00
permissions on any inventory . This is primarily used to decide if the user can create
2013-04-02 22:59:58 +04:00
host or group objects
2013-03-26 22:44:12 +04:00
'''
if user . is_superuser :
return True
by_org_admin = user . organizations . filter (
admins__in = [ user ]
) . count ( )
by_team_permission = Permission . objects . filter (
team__in = user . teams . all ( ) ,
permission_type__in = allowed
) . count ( )
by_user_permission = user . permissions . filter (
permission_type__in = allowed
) . count ( )
2013-04-02 22:59:58 +04:00
2013-03-26 22:44:12 +04:00
result = ( by_org_admin + by_team_permission + by_user_permission )
return result > 0
2013-03-26 03:00:07 +04:00
@classmethod
def can_user_add ( cls , user , data ) :
if not ' organization ' in data :
2013-03-26 22:44:12 +04:00
return True
2013-03-26 03:00:07 +04:00
if user . is_superuser :
return True
if not user . is_superuser :
org = Organization . objects . get ( pk = data [ ' organization ' ] )
if user in org . admins . all ( ) :
return True
return False
2013-03-26 01:21:17 +04:00
@classmethod
def can_user_administrate ( cls , user , obj ) :
return cls . _has_permission_types ( user , obj , PERMISSION_TYPES_ALLOWING_INVENTORY_ADMIN )
2013-03-27 03:21:18 +04:00
@classmethod
def can_user_attach ( cls , user , obj , sub_obj , relationship_type ) :
''' whether you can add sub_obj to obj using the relationship type in a subobject view '''
2013-03-28 02:54:30 +04:00
if not sub_obj . can_user_read ( user , sub_obj ) :
return False
return cls . _has_permission_types ( user , obj , PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE )
@classmethod
def can_user_unattach ( cls , user , obj , sub_obj , relationship ) :
2013-03-27 03:21:18 +04:00
return cls . _has_permission_types ( user , obj , PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE )
2013-03-26 01:21:17 +04:00
@classmethod
def can_user_read ( cls , user , obj ) :
return cls . _has_permission_types ( user , obj , PERMISSION_TYPES_ALLOWING_INVENTORY_READ )
@classmethod
def can_user_delete ( cls , user , obj ) :
return cls . _has_permission_types ( user , obj , PERMISSION_TYPES_ALLOWING_INVENTORY_ADMIN )
2013-03-26 22:44:12 +04:00
class Host ( CommonModelNameNotUnique ) :
2013-03-13 21:09:36 +04:00
'''
A managed node
'''
2013-03-24 23:54:57 +04:00
2013-03-14 00:29:51 +04:00
class Meta :
app_label = ' main '
2013-03-27 00:57:08 +04:00
unique_together = ( ( " name " , " inventory " ) , )
2013-04-02 22:59:58 +04:00
2013-03-27 06:24:03 +04:00
variable_data = models . OneToOneField ( ' VariableData ' , null = True , default = None , blank = True , on_delete = SET_NULL , related_name = ' host ' )
inventory = models . ForeignKey ( ' Inventory ' , null = False , 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-27 02:18:05 +04:00
@classmethod
def can_user_read ( cls , user , obj ) :
return Inventory . can_user_read ( user , obj . inventory )
2013-04-02 22:59:58 +04:00
2013-03-26 22:44:12 +04:00
@classmethod
def can_user_add ( cls , user , data ) :
if not ' inventory ' in data :
return False
inventory = Inventory . objects . get ( pk = data [ ' inventory ' ] )
2013-03-27 03:21:18 +04:00
rc = Inventory . _has_permission_types ( user , inventory , PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE )
return rc
2013-03-26 22:44:12 +04:00
def get_absolute_url ( self ) :
import lib . urls
return reverse ( lib . urls . views_HostsDetail , args = ( self . pk , ) )
2013-04-02 21:11:07 +04:00
# relationship to LaunchJobStatus
# relationship to LaunchJobStatusEvent
# last_job_status
2013-03-26 22:44:12 +04:00
class Group ( CommonModelNameNotUnique ) :
2013-03-23 02:55:10 +04:00
2013-03-13 21:09:36 +04:00
'''
A group of managed nodes . May belong to multiple groups
'''
2013-03-24 23:54:57 +04:00
2013-03-14 00:29:51 +04:00
class Meta :
app_label = ' main '
2013-03-27 00:57:08 +04:00
unique_together = ( ( " name " , " inventory " ) , )
2013-03-13 21:09:36 +04:00
2013-03-27 06:24:03 +04:00
inventory = models . ForeignKey ( ' Inventory ' , null = False , related_name = ' groups ' )
parents = models . ManyToManyField ( ' self ' , symmetrical = False , related_name = ' children ' , blank = True )
variable_data = models . OneToOneField ( ' VariableData ' , null = True , default = None , blank = True , on_delete = SET_NULL , related_name = ' group ' )
2013-03-29 10:36:11 +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-26 22:44:12 +04:00
@classmethod
def can_user_add ( cls , user , data ) :
if not ' inventory ' in data :
return False
inventory = Inventory . objects . get ( pk = data [ ' inventory ' ] )
return Inventory . _has_permission_types ( user , inventory , PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE )
2013-04-02 22:59:58 +04:00
2013-03-28 02:17:21 +04:00
@classmethod
def can_user_administrate ( cls , user , obj ) :
# here this controls whether the user can attach subgroups
return Inventory . _has_permission_types ( user , obj . inventory , PERMISSION_TYPES_ALLOWING_INVENTORY_WRITE )
@classmethod
def can_user_read ( cls , user , obj ) :
return Inventory . can_user_read ( user , obj . inventory )
2013-03-27 00:57:08 +04:00
def get_absolute_url ( self ) :
import lib . urls
return reverse ( lib . urls . views_GroupsDetail , args = ( self . pk , ) )
2013-03-13 23:15:35 +04:00
# FIXME: audit nullables
# FIXME: audit cascades
2013-03-13 21:09:36 +04:00
2013-03-27 00:57:08 +04:00
class VariableData ( CommonModelNameNotUnique ) :
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-24 23:54:57 +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-27 06:24:03 +04:00
#host = models.OneToOneField('Host', null=True, default=None, blank=True, on_delete=SET_NULL, related_name='variable_data')
#group = models.OneToOneField('Group', null=True, default=None, blank=True, on_delete=SET_NULL, 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-27 06:24:03 +04:00
def get_absolute_url ( self ) :
import lib . urls
return reverse ( lib . urls . views_VariableDetail , args = ( self . pk , ) )
@classmethod
def can_user_read ( cls , user , obj ) :
''' a user can be read if they are on the same team or can be administrated '''
if obj . host is not None :
return Inventory . can_user_read ( user , obj . host . inventory )
if obj . group is not None :
return Inventory . can_user_read ( user , obj . group . inventory )
return False
2013-04-04 20:07:12 +04:00
class Credential ( CommonModelNameNotUnique ) :
2013-03-13 21:09:36 +04:00
'''
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-24 23:54:57 +04:00
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
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-04-02 19:23:58 +04:00
# IF ssh_key_path is SET
#
# STAGE 1: SSH KEY SUPPORT
#
# ssh-agent bash &
# save keyfile to tempdir in /var/tmp (permissions guarded)
# ssh-add path-to-keydata
# key could locked or unlocked, so use 'expect like' code to enter it at the prompt
# if key is locked:
# if ssh_key_unlock is provided provide key password
# if not provided, FAIL
#
# default_username if set corresponds to -u on ansible-playbook, if unset -u root
#
# STAGE 2:
# OR if ssh_password is set instead, do not use SSH agent
# set ANSIBLE_SSH_PASSWORD
#
# STAGE 3:
#
2013-04-02 22:59:58 +04:00
# MICHAEL: modify ansible/ansible-playbook such that
2013-04-02 19:23:58 +04:00
# if ANSIBLE_PASSWORD or ANSIBLE_SUDO_PASSWORD is set
# you do not have to use --ask-pass and --ask-sudo-pass, so we don't have to do interactive
# stuff with that.
#
# ansible-playbook foo.yml ...
ssh_key_data = models . TextField ( blank = True , default = ' ' )
ssh_key_unlock = models . CharField ( blank = True , default = ' ' , max_length = 1024 )
2013-04-02 22:59:58 +04:00
default_username = models . CharField ( blank = True , default = ' ' , max_length = 1024 )
2013-04-02 19:23:58 +04:00
ssh_password = models . CharField ( blank = True , default = ' ' , max_length = 1024 )
sudo_password = models . CharField ( blank = True , default = ' ' , max_length = 1024 )
2013-03-24 23:54:57 +04:00
2013-04-02 22:59:58 +04:00
@classmethod
2013-04-04 20:07:12 +04:00
def can_user_administrate ( cls , user , obj ) :
2013-04-02 22:59:58 +04:00
if user . is_superuser :
return True
if user == obj . user :
return True
2013-04-08 06:17:33 +04:00
2013-04-08 03:57:16 +04:00
if obj . user :
if ( obj . user . organizations . filter ( admins__in = [ user ] ) . count ( ) ) :
return True
if obj . team :
if user in obj . team . organization . admins . all ( ) :
return True
2013-04-02 22:59:58 +04:00
return False
2013-04-16 03:19:54 +04:00
@classmethod
def can_user_delete ( cls , user , obj ) :
if obj . user is None and obj . team is None :
# unassociated credentials may be marked deleted by anyone
return True
return cls . can_user_administrate ( user , obj )
2013-04-04 20:07:12 +04:00
@classmethod
def can_user_read ( cls , user , obj ) :
''' a user can be read if they are on the same team or can be administrated '''
return cls . can_user_administrate ( user , obj )
2013-04-08 06:17:33 +04:00
2013-04-04 20:07:12 +04:00
@classmethod
def can_user_add ( cls , user , data ) :
if user . is_superuser :
return True
if ' user ' in data :
user_obj = User . objects . get ( pk = data [ ' user ' ] )
return UserHelper . can_user_administrate ( user , user_obj )
if ' team ' in data :
2013-04-04 22:41:31 +04:00
team_obj = Team . objects . get ( pk = data [ ' team ' ] )
return Team . can_user_administrate ( user , team_obj )
2013-04-04 20:07:12 +04:00
2013-04-02 22:59:58 +04:00
def get_absolute_url ( self ) :
import lib . urls
return reverse ( lib . urls . views_CredentialsDetail , args = ( self . pk , ) )
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-24 23:54:57 +04:00
2013-03-14 00:29:51 +04:00
class Meta :
app_label = ' main '
2013-03-24 23:54:57 +04:00
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-04-02 01:44:06 +04:00
organization = models . ForeignKey ( ' Organization ' , blank = False , null = True , on_delete = SET_NULL , related_name = ' teams ' )
2013-03-13 21:09:36 +04:00
2013-04-01 06:18:39 +04:00
def get_absolute_url ( self ) :
import lib . urls
return reverse ( lib . urls . views_TeamsDetail , args = ( self . pk , ) )
2013-04-02 00:04:27 +04:00
@classmethod
def can_user_administrate ( cls , user , obj ) :
2013-04-02 01:44:06 +04:00
# FIXME -- audit when this is called explicitly, if any
2013-04-02 00:04:27 +04:00
if user . is_superuser :
return True
2013-04-04 20:07:12 +04:00
if user in obj . organization . admins . all ( ) :
2013-04-02 00:04:27 +04:00
return True
return False
@classmethod
def can_user_read ( cls , user , obj ) :
if cls . can_user_administrate ( user , obj ) :
return True
if obj . users . filter ( pk__in = [ user . pk ] ) . count ( ) :
return True
return False
@classmethod
def can_user_add ( cls , user , data ) :
if user . is_superuser :
return True
if Organization . objects . filter ( admins__in = [ user ] ) . count ( ) :
# team assignment to organizations is handled elsewhere, this just creates
# a blank team
return True
return False
2013-04-02 01:44:06 +04:00
@classmethod
def can_user_delete ( cls , user , obj ) :
return cls . can_user_administrate ( user , obj )
2013-04-02 22:59:58 +04:00
2013-03-13 21:09:36 +04:00
class Project ( CommonModel ) :
2013-03-24 23:54:57 +04:00
'''
2013-03-13 21:09:36 +04:00
A project represents a playbook git repo that can access a set of inventories
2013-03-24 23:54:57 +04:00
'''
2013-04-15 19:31:54 +04:00
# this is not part of the project, but managed with perms
# 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-24 23:54:57 +04:00
@classmethod
2013-03-22 23:53:08 +04:00
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-04-01 05:18:16 +04:00
@classmethod
def can_user_delete ( cls , user , obj ) :
return cls . can_user_administrate ( user , obj )
2013-04-01 04:02:56 +04:00
2013-03-26 22:51:14 +04:00
class Permission ( CommonModelNameNotUnique ) :
2013-03-13 21:09:36 +04:00
'''
A permission allows a user , project , or team to be able to use an inventory source .
'''
2013-03-24 23:54:57 +04:00
2013-03-14 00:29:51 +04:00
class Meta :
app_label = ' main '
2013-03-13 21:09:36 +04:00
2013-03-26 01:36:51 +04:00
# permissions are granted to either a user or a team:
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
team = models . ForeignKey ( ' Team ' , null = True , on_delete = SET_NULL , blank = True , related_name = ' permissions ' )
2013-03-26 01:36:51 +04:00
# to be used against a project or inventory (or a project and inventory in conjunction):
project = models . ForeignKey ( ' Project ' , null = True , on_delete = SET_NULL , blank = True , related_name = ' permissions ' )
inventory = models . ForeignKey ( ' Inventory ' , null = True , on_delete = SET_NULL , related_name = ' permissions ' )
# permission system explanation:
#
# for example, user A on inventory X has write permissions (PERM_INVENTORY_WRITE)
# team C on inventory X has read permissions (PERM_INVENTORY_READ)
# team C on inventory X and project Y has launch permissions (PERM_INVENTORY_DEPLOY)
2013-04-02 22:59:58 +04:00
# team C on inventory X and project Z has dry run permissions (PERM_INVENTORY_CHECK)
2013-03-26 01:36:51 +04:00
#
# basically for launching, permissions can be awarded to the whole inventory source or just the inventory source
# in context of a given project.
#
# the project parameter is not used when dealing with READ, WRITE, or ADMIN permissions.
2013-03-26 00:41:21 +04:00
permission_type = models . CharField ( max_length = 64 , choices = PERMISSION_TYPE_CHOICES )
2013-04-02 22:59:58 +04:00
2013-03-27 00:57:08 +04:00
def __unicode__ ( self ) :
return unicode ( " Permission(name= %s ,ON(user= %s ,team= %s ),FOR(project= %s ,inventory= %s ,type= %s )) " % (
self . name ,
2013-04-02 22:59:58 +04:00
self . user ,
self . team ,
self . project ,
self . inventory ,
2013-03-27 00:57:08 +04:00
self . permission_type
) )
2013-03-26 01:36:51 +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
'''
2013-03-29 10:36:11 +04:00
A launch job is a definition for applying a project ( with playbook ) to an
inventory source with a given credential .
2013-03-24 23:54:57 +04:00
'''
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-26 00:41:21 +04:00
# JOB_TYPE_CHOICES are a subset of PERMISSION_TYPE_CHOICES
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 ) :
2013-03-29 10:36:11 +04:00
'''
Create a new launch job status and start the task via celery .
'''
2013-03-23 02:55:10 +04:00
from lib . main . tasks import run_launch_job
2013-03-29 10:36:11 +04:00
launch_job_status = self . launch_job_statuses . create ( name = ' Launch Job Status %s ' % now ( ) . isoformat ( ) )
2013-03-29 09:02:07 +04:00
task_result = run_launch_job . delay ( launch_job_status . pk )
2013-04-05 00:53:20 +04:00
# The TaskMeta instance in the database isn't created until the worker
# starts processing the task, so we can only store the task ID here.
launch_job_status . celery_task_id = task_result . task_id
launch_job_status . save ( update_fields = [ ' celery_task_id ' ] )
2013-03-29 09:02:07 +04:00
return launch_job_status
2013-03-23 02:55:10 +04:00
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-24 23:54:57 +04:00
2013-03-22 19:49:04 +04:00
# ssh-agent bash
# ssh-add ... < key entry
#
2013-03-24 23:54:57 +04:00
# playbook in source control is already on the disk
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
2013-03-24 23:54:57 +04:00
2013-03-22 20:00:11 +04:00
# 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>
2013-03-13 23:15:35 +04:00
class LaunchJobStatus ( CommonModel ) :
2013-03-29 09:02:07 +04:00
'''
Status for a single run of a launch job .
'''
STATUS_CHOICES = [
( ' pending ' , _ ( ' Pending ' ) ) ,
( ' running ' , _ ( ' Running ' ) ) ,
( ' successful ' , _ ( ' Successful ' ) ) ,
( ' failed ' , _ ( ' Failed ' ) ) ,
2013-04-05 00:53:20 +04:00
( ' error ' , _ ( ' Error ' ) ) ,
2013-03-29 09:02:07 +04:00
]
2013-03-24 23:54:57 +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 = _ ( ' launch job statuses ' )
2013-03-13 23:15:35 +04:00
2013-04-05 00:53:20 +04:00
launch_job = models . ForeignKey ( ' LaunchJob ' , null = True , on_delete = SET_NULL , related_name = ' launch_job_statuses ' )
status = models . CharField ( max_length = 20 , choices = STATUS_CHOICES , default = ' pending ' )
result_stdout = models . TextField ( blank = True , default = ' ' )
result_stderr = models . TextField ( blank = True , default = ' ' )
result_traceback = models . TextField ( blank = True , default = ' ' )
celery_task_id = models . CharField ( max_length = 100 , blank = True , default = ' ' , editable = False )
2013-04-16 03:22:57 +04:00
hosts = models . ManyToManyField ( ' Host ' , related_name = ' launch_job_statuses ' , blank = True , through = ' LaunchJobHostSummary ' )
2013-04-05 00:53:20 +04:00
@property
def celery_task ( self ) :
try :
if self . celery_task_id :
return TaskMeta . objects . get ( task_id = self . celery_task_id )
except TaskMeta . DoesNotExist :
pass
2013-03-29 09:02:07 +04:00
2013-04-16 03:22:57 +04:00
def save ( self , * args , * * kwargs ) :
super ( LaunchJobStatus , self ) . save ( * args , * * kwargs )
# Create a new host summary for each host in the inventory.
for host in self . launch_job . inventory . hosts . all ( ) :
# Due to the way the inventory script is called, hosts without a group won't be affected.
if host . groups . count ( ) :
self . launch_job_host_summaries . get_or_create ( host = host )
class LaunchJobHostSummary ( models . Model ) :
class Meta :
unique_together = [ ( ' launch_job_status ' , ' host ' ) ]
verbose_name_plural = _ ( ' Launch Job Host Summaries ' )
ordering = ( ' -pk ' , )
launch_job_status = models . ForeignKey ( ' LaunchJobStatus ' , on_delete = models . CASCADE , related_name = ' launch_job_host_summaries ' )
host = models . ForeignKey ( ' Host ' , on_delete = models . CASCADE , related_name = ' launch_job_host_summaries ' )
# FIXME: Can't use SET_NULL for host relationship because of unique constraint.
changed = models . PositiveIntegerField ( default = 0 )
dark = models . PositiveIntegerField ( default = 0 )
failures = models . PositiveIntegerField ( default = 0 )
ok = models . PositiveIntegerField ( default = 0 )
processed = models . PositiveIntegerField ( default = 0 )
skipped = models . PositiveIntegerField ( default = 0 )
def __unicode__ ( self ) :
return ' %s changed= %d dark= %d failures= %d ok= %d processed= %d skipped= %s ' % \
( self . host . name , self . changed , self . dark , self . failures , self . ok ,
self . processed , self . skipped )
2013-03-29 09:02:07 +04:00
class LaunchJobStatusEvent ( models . Model ) :
'''
2013-03-29 10:36:11 +04:00
An event / message logged from the callback when running a job .
2013-03-29 09:02:07 +04:00
'''
2013-03-24 23:54:57 +04:00
2013-03-29 09:02:07 +04:00
EVENT_TYPES = [
( ' runner_on_failed ' , _ ( ' Runner on Failed ' ) ) ,
( ' runner_on_ok ' , _ ( ' Runner on OK ' ) ) ,
( ' runner_on_error ' , _ ( ' Runner on Error ' ) ) ,
( ' runner_on_skipped ' , _ ( ' Runner on Skipped ' ) ) ,
( ' runner_on_unreachable ' , _ ( ' Runner on Unreachable ' ) ) ,
( ' runner_on_no_hosts ' , _ ( ' Runner on No Hosts ' ) ) ,
( ' runner_on_async_poll ' , _ ( ' Runner on Async Poll ' ) ) ,
( ' runner_on_async_ok ' , _ ( ' Runner on Async OK ' ) ) ,
( ' runner_on_async_failed ' , _ ( ' Runner on Async Failed ' ) ) ,
( ' playbook_on_start ' , _ ( ' Playbook on Start ' ) ) ,
( ' playbook_on_notify ' , _ ( ' Playbook on Notify ' ) ) ,
( ' playbook_on_task_start ' , _ ( ' Playbook on Task Start ' ) ) ,
( ' playbook_on_vars_prompt ' , _ ( ' Playbook on Vars Prompt ' ) ) ,
( ' playbook_on_setup ' , _ ( ' Playbook on Setup ' ) ) ,
( ' playbook_on_import_for_host ' , _ ( ' Playbook on Import for Host ' ) ) ,
( ' playbook_on_not_import_for_host ' , _ ( ' Playbook on Not Import for Host ' ) ) ,
( ' playbook_on_play_start ' , _ ( ' Playbook on Play Start ' ) ) ,
( ' playbook_on_stats ' , _ ( ' Playbook on Stats ' ) ) ,
]
2013-03-13 23:15:35 +04:00
2013-03-29 09:02:07 +04:00
class Meta :
app_label = ' main '
2013-03-13 23:15:35 +04:00
2013-03-29 10:36:11 +04:00
launch_job_status = models . ForeignKey ( ' LaunchJobStatus ' , related_name = ' launch_job_status_events ' , on_delete = CASCADE )
2013-03-29 09:02:07 +04:00
created = models . DateTimeField ( auto_now_add = True )
event = models . CharField ( max_length = 100 , choices = EVENT_TYPES )
event_data = JSONField ( blank = True , default = ' ' )
2013-04-05 00:53:20 +04:00
host = models . ForeignKey ( ' Host ' , blank = True , null = True , default = None , on_delete = SET_NULL , related_name = ' launch_job_status_events ' )
2013-04-08 06:17:33 +04:00
2013-04-16 03:22:57 +04:00
def __unicode__ ( self ) :
return u ' %s @ %s ' % ( self . get_event_display ( ) , self . created . isoformat ( ) )
def save ( self , * args , * * kwargs ) :
try :
if not self . host and self . event_data . get ( ' host ' , ' ' ) :
# Make sure we're looking at only the hosts from this launch job's associated inventory.
self . host = self . launch_job_status . launch_job . inventory . hosts . get ( name = self . event_data [ ' host ' ] )
except ( Host . DoesNotExist , AttributeError ) :
pass
super ( LaunchJobStatusEvent , self ) . save ( * args , * * kwargs )
self . update_host_summary_from_stats ( )
def update_host_summary_from_stats ( self ) :
if self . event != ' playbook_on_stats ' :
return
hostnames = set ( )
for v in self . event_data . values ( ) :
hostnames . update ( v . keys ( ) )
for hostname in hostnames :
try :
host = self . launch_job_status . launch_job . inventory . hosts . get ( name = hostname )
except Host . DoesNotExist :
continue
host_summary = self . launch_job_status . launch_job_host_summaries . get_or_create ( host = host ) [ 0 ]
host_summary_changed = False
for stat in ( ' changed ' , ' dark ' , ' failures ' , ' ok ' , ' processed ' , ' skipped ' ) :
value = self . event_data . get ( stat , { } ) . get ( hostname , 0 )
if getattr ( host_summary , stat ) != value :
setattr ( host_summary , stat , value )
host_summary_changed = True
if host_summary_changed :
host_summary . save ( )
2013-03-01 04:39:01 +04:00
2013-03-29 09:02:07 +04:00
# TODO: reporting (MPD)
2013-04-10 08:41:51 +04:00
@receiver ( post_save , sender = User )
def create_auth_token_for_user ( sender , * * kwargs ) :
instance = kwargs . get ( ' instance ' , None )
if instance :
try :
Token . objects . get_or_create ( user = instance )
except DatabaseError :
pass # Only fails when creating a new superuser from syncdb on a
# new database (before migrate has been called).