class FoundationBluePrint(BluePrint): parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE) foundation_type_list = StringListField( max_length=200 ) # list of the foundation types this blueprint can be used for template = JSONField(default={}, blank=True) physical_interface_names = StringListField(max_length=200, blank=True) @cinp.action('Map') def getConfig(self): return getConfig(self) @property def subcontractor(self): return {'type': None} @cinp.check_auth() @staticmethod def checkAuth(user, method, id_list, action=None): return True def clean(self, *args, **kwargs): super().clean(*args, **kwargs) errors = {} if not isinstance(self.template, dict): errors['template'] = 'template must be a dict' if errors: raise ValidationError(errors) def __str__(self): return 'FoundationBluePrint "{0}"({1})'.format(self.description, self.name)
class BaseJob(models.Model): JOB_STATE_CHOICES = (('queued', 'queued'), ('waiting', 'waiting'), ('done', 'done'), ('paused', 'paused'), ('error', 'error'), ('aborted', 'aborted')) site = models.ForeignKey(Site, editable=False, on_delete=models.CASCADE) state = models.CharField(max_length=10, choices=JOB_STATE_CHOICES) status = JSONField(default=[], blank=True) message = models.CharField(max_length=1024, default='', blank=True) script_runner = models.BinaryField(editable=False) script_name = models.CharField(max_length=40, editable=False, default=False) updated = models.DateTimeField(editable=False, auto_now=True) created = models.DateTimeField(editable=False, auto_now_add=True) @property def realJob(self): try: return self.foundationjob except ObjectDoesNotExist: pass try: return self.structurejob except ObjectDoesNotExist: pass try: return self.dependancyjob except ObjectDoesNotExist: pass return self def pause(self): if self.state != 'queued': raise ValueError('Can only pause a job if it is queued') self.state = 'paused' self.save() def resume(self): if self.state != 'paused': raise ValueError('Can only resume a job if it is paused') self.state = 'queued' self.save() def reset(self): if self.state != 'error': raise ValueError('Can only reset a job if it is in error') runner = pickle.loads(self.script_runner) runner.clearDispatched() self.status = runner.status self.script_runner = pickle.dumps(runner) self.state = 'queued' self.save() def rollback(self): if self.state != 'error': raise ValueError('Can only rollback a job if it is in error') runner = pickle.loads(self.script_runner) msg = runner.rollback() if msg != 'Done': raise ValueError('Unable to rollback "{0}"'.format(msg)) self.status = runner.status self.script_runner = pickle.dumps(runner) self.state = 'queued' self.save() @property def progress(self): try: return self.status[0][0] except IndexError: return 0.0 @cinp.check_auth() @staticmethod def checkAuth(user, method, id_list, action=None): if method == 'DESCRIBE': return True return False def clean(self, *args, **kwargs ): # also need to make sure a Structure is in only one complex super().clean(*args, **kwargs) errors = {} if self.state not in self.JOB_STATE_CHOICES: errors['state'] = 'Invalid state "{0}"'.format(self.state) if errors: raise ValidationError(errors) def __str__(self): return 'BaseJob #{0} in "{1}"'.format(self.pk, self.site.pk)
class Foundation( models.Model ): locator = models.CharField( max_length=100, primary_key=True ) # if this changes make sure to update architect - instance - foundation_id site = models.ForeignKey( Site, on_delete=models.PROTECT ) blueprint = models.ForeignKey( FoundationBluePrint, on_delete=models.PROTECT ) id_map = JSONField( blank=True, null=True ) # ie a dict of asset, chassis, system, etc types located_at = models.DateTimeField( editable=False, blank=True, null=True ) built_at = models.DateTimeField( editable=False, blank=True, null=True ) updated = models.DateTimeField( editable=False, auto_now=True ) created = models.DateTimeField( editable=False, auto_now_add=True ) @cinp.action( return_type={ 'type': 'String' }, paramater_type_list=[ 'Map' ] ) def setIdMap( self, id_map ): error = self.blueprint.validateIdMap( id_map ) if error is not None: return error self.id_map = id_map self.full_clean() self.save() for iface in RealNetworkInterface.objects.filter( foundation=self ): if iface.physical_location in id_map[ 'network' ]: iface.mac = id_map[ 'network' ][ iface.physical_location ][ 'mac' ] iface.full_clean() iface.save() return None def _canSetState( self, job=None ): # You can't set state if there is a related job, unless that job (that is hopfully being passed in from the job that is calling this) is that related job try: self.cartographer return False except ObjectDoesNotExist: pass try: return self.foundationjob == job except ObjectDoesNotExist: pass try: self.structure.structurejob return False except ( ObjectDoesNotExist, AttributeError ): pass return True def setLocated( self ): """ Sets the Foundation to 'located' state. This will not create/destroy any jobs. NOTE: This will set the attached structure (if there is one) to 'planned' without running a job to destroy the structure. """ try: job = self.foundationjob if job.script_name != 'create': job = None except ObjectDoesNotExist: job = None if not self._canSetState( job ): raise Exception( 'All related jobs and cartographer instances must be cleared before setting Located' ) if self.structure is not None and self.structure.state != 'planned': raise Exception( 'Attached Structure must be in Planned state' ) template = self.blueprint.getTemplate() if template is not None and not self.id_map: raise Exception( 'Foundations with blueprints, which specify templates, require id_map to be set before setting to Located' ) self.located_at = timezone.now() self.built_at = None self.full_clean() self.save() def setBuilt( self, job=None ): """ Set the Foundation to 'built' state. This will not create/destroy any jobs. """ if not self._canSetState( job ): raise Exception( 'All related jobs and cartographer instances must be cleared before setting Built' ) if self.located_at is None: if self.blueprint.getTemplate() is not None: raise Exception( 'Foundation with Blueprints with templates must be located first' ) self.located_at = timezone.now() self.built_at = timezone.now() self.full_clean() self.save() def setDestroyed( self, job=None ): """ Sets the Foundation to 'destroyed' state. This will not create/destroy any jobs. NOTE: This will set the attached structure (if there is one) to 'planned' without running a job to destroy the structure. """ if not self._canSetState( job ): raise Exception( 'All related jobs and cartographer instances must be cleared before setting Destroyed' ) try: self.structure.setDestroyed() # TODO: this may be a little harsh except AttributeError: pass for iface in RealNetworkInterface.objects.filter( foundation=self ): iface.mac = None iface.full_clean() iface.save() self.built_at = None self.located_at = None self.id_map = None self.full_clean() self.save() @cinp.action( return_type='Integer', paramater_type_list=[ '_USER_' ] ) def doCreate( self, user ): """ This will submit a job to run the create script. """ from contractor.Foreman.lib import createJob return createJob( 'create', self, user ) @cinp.action( return_type='Integer', paramater_type_list=[ '_USER_' ] ) def doDestroy( self, user ): """ This will submit a job to run the destroy script. """ from contractor.Foreman.lib import createJob return createJob( 'destroy', self, user ) @cinp.action( return_type='Integer', paramater_type_list=[ '_USER_', 'String' ] ) def doJob( self, user, name ): """ This will submit a job to run the specified script. """ from contractor.Foreman.lib import createJob if name in ( 'create', 'destroy' ): raise ValueError( 'Invalid Job Name' ) return createJob( name, self, user ) @cinp.action( return_type={ 'type': 'Model', 'model': 'contractor.Foreman.models.FoundationJob' } ) def getJob( self ): """ Return the Job for this Foundation if there is one """ try: return self.foundationjob except ObjectDoesNotExist: pass return None @staticmethod def getTscriptValues( write_mode=False ): # locator is handled seperatly return { # none of these base items are writeable, ignore the write_mode for now 'locator': ( lambda foundation: foundation.locator, None ), # redundant? 'type': ( lambda foundation: foundation.subclass.type, None ), # redudnant? 'site': ( lambda foundation: foundation.site.pk, None ), 'blueprint': ( lambda foundation: foundation.blueprint.pk, None ), # 'ip_map': ( lambda foundation: foundation.ip_map, None ), 'interface_list': ( lambda foundation: [ i for i in foundation.networkinterface_set.all().order_by( 'physical_location' ) ], None ) # redudntant? } @staticmethod def getTscriptFunctions(): return {} def configAttributes( self ): provisioning_interface = self.provisioning_interface return { '_provisioning_interface': provisioning_interface.physical_location if provisioning_interface is not None else None, # what ever deals with the provisioning interface will have to deal with the physical_location, otherwise tools that are not the final target OS will be confused '_provisioning_interface_mac': provisioning_interface.mac if provisioning_interface is not None else None, '_foundation_id': self.pk, '_foundation_type': self.type, '_foundation_state': self.state, '_foundation_class_list': self.class_list, '_foundation_locator': self.locator, '_foundation_id_map': self.id_map, '_foundation_interface_list': [ i.config for i in self.networkinterface_set.all().order_by( 'physical_location' ) ] } @property def provisioning_interface( self ): try: return self.networkinterface_set.get( is_provisioning=True ) except ObjectDoesNotExist: return None @property def subclass( self ): for attr in FOUNDATION_SUBCLASS_LIST: try: return getattr( self, attr ) except AttributeError: pass return self @property def type( self ): real = self.subclass if real != self: return real.type return 'Unknown' @property def class_list( self ): # top level generic classes: Metal, VM, Container, Switch, PDU return [] @property def complex( self ): return None @property def can_delete( self ): try: return self.structure.state != 'build' except AttributeError: pass return True @property def structure( self ): try: return Structure.objects.get( foundation=self ) except Structure.DoesNotExist: pass return None @property def state( self ): if self.located_at is not None and self.built_at is not None: return 'built' elif self.located_at is not None: return 'located' return 'planned' @property def description( self ): return self.locator @property def dependency( self ): try: return Dependency.objects.get( foundation=self ) except Dependency.DoesNotExist: return None @property def dependencyId( self ): return 'f-{0}'.format( self.pk ) @cinp.action( { 'type': 'String', 'is_array': True } ) @staticmethod def getFoundationTypes(): return FOUNDATION_SUBCLASS_LIST @cinp.action( return_type='Map' ) def getConfig( self ): """ returns the computed config for this foundation """ return mergeValues( getConfig( self.subclass ) ) @cinp.action( return_type={ 'type': 'Map', 'is_array': True } ) def getInterfaceList( self ): """ returns the computed config for this foundation """ return [ { 'name': i.name, 'physical_location': i.physical_location, 'is_provisioning': i.is_provisioning, 'mac': i.mac, 'pxe': i.pxe } for i in self.networkinterface_set.all().order_by( 'physical_location' ) ] @cinp.list_filter( name='site', paramater_type_list=[ { 'type': 'Model', 'model': Site } ] ) @staticmethod def filter_site( site ): return Foundation.objects.filter( site=site ) @cinp.list_filter( name='todo', paramater_type_list=[ { 'type': 'Model', 'model': Site }, 'Boolean', 'String' ] ) @staticmethod def filter_todo( site, has_dependancies, foundation_class ): args = {} args[ 'site' ] = site if has_dependancies: args[ 'dependency' ] = True if foundation_class is not None: if foundation_class not in FOUNDATION_SUBCLASS_LIST: raise ValueError( 'Invalid foundation class' ) args[ foundation_class ] = True return Foundation.objects.filter( **args ) @cinp.check_auth() @staticmethod def checkAuth( user, verb, id_list, action=None ): return True def clean( self, *args, **kwargs ): super().clean( *args, **kwargs ) errors = {} if not name_regex.match( self.locator ): errors[ 'locator' ] = 'Invalid' if self.blueprint_id is not None and self.type not in self.blueprint.foundation_type_list: errors[ 'name' ] = 'Blueprint "{0}" does not list this type ({1})'.format( self.blueprint, self.type ) if errors: raise ValidationError( errors ) def delete( self ): if not self.can_delete: raise models.ProtectedError( 'Structure not Deleatable', self ) subclass = self.subclass if self == subclass: super().delete() else: subclass.delete() def __str__( self ): return 'Foundation #{0}({1}) in "{2}"'.format( self.pk, self.locator, self.site.pk )
class testModel( models.Model ): f = JSONField()
class testModel( models.Model ): f = JSONField( default=None, null=True, blank=True )
class BaseJob(models.Model): JOB_STATE_CHOICES = ('queued', 'waiting', 'done', 'paused', 'error', 'aborted') site = models.ForeignKey(Site, editable=False, on_delete=models.CASCADE) state = models.CharField(max_length=10, choices=[(i, i) for i in JOB_STATE_CHOICES]) status = JSONField(default=[], blank=True) message = models.CharField(max_length=1024, default='', blank=True) script_runner = models.BinaryField(editable=False) script_name = models.CharField(max_length=40, editable=False, default=False) updated = models.DateTimeField(editable=False, auto_now=True) created = models.DateTimeField(editable=False, auto_now_add=True) @property def realJob(self): try: return self.foundationjob except ObjectDoesNotExist: pass try: return self.structurejob except ObjectDoesNotExist: pass try: return self.dependencyjob except ObjectDoesNotExist: pass return self @property def progress(self): try: return self.status[0][0] except IndexError: return 0.0 @property def can_start(self): return False @cinp.action() def pause(self): """ Pause a job that is in 'queued' state state. Errors: NOT_PAUSEABLE - Job is not in state 'queued'. """ if self.state != 'queued': raise ForemanException('NOT_PAUSEABLE', 'Can only pause a job if it is queued') self.state = 'paused' self.full_clean() self.save() @cinp.action() def resume(self): """ Resume a job that is in 'paused' state state. Errors: NOT_PAUSED - Job is not in state 'paused'. """ if self.state != 'paused': raise ForemanException('NOT_PAUSED', 'Can only resume a job if it is paused') self.state = 'queued' self.full_clean() self.save() @cinp.action() def reset(self): """ Resets a job that is in 'error' state, this allows the job to try the failed step again. Errors: NOT_ERRORED - Job is not in state 'error'. """ if self.state != 'error': raise ForemanException('NOT_ERRORED', 'Can only reset a job if it is in error') runner = pickle.loads(self.script_runner) runner.clearDispatched() self.status = runner.status self.script_runner = pickle.dumps(runner, protocol=PICKLE_PROTOCOL) self.state = 'queued' self.full_clean() self.save() @cinp.action() def rollback(self): """ Starts the rollback for jobs that are in state 'error'. Errors: NOT_ERRORED - Job is not in state 'error'. """ if self.state != 'error': raise ForemanException( 'NOT_ERRORED', 'Can only rollback a job if it is in error') runner = pickle.loads(self.script_runner) msg = runner.rollback() if msg != 'Done': raise ValueError('Unable to rollback "{0}"'.format(msg)) self.status = runner.status self.script_runner = pickle.dumps(runner, protocol=PICKLE_PROTOCOL) self.state = 'queued' self.full_clean() self.save() @cinp.action() def clearDispatched(self): """ Resets a job that is in 'queued' state, and subcontractor lost the job. Make sure to verify that subcontractor has lost the job results before calling this. Errors: NOT_ERRORED - Job is not in state 'queued'. """ if self.state != 'queued': raise ForemanException( 'NOT_ERRORED', 'Can only clear the dispatched flag a job if it is in queued state' ) runner = pickle.loads(self.script_runner) runner.clearDispatched() self.status = runner.status self.script_runner = pickle.dumps(runner, protocol=PICKLE_PROTOCOL) self.full_clean() self.save() @cinp.action(return_type={'type': 'Map'}, paramater_type_list=[{ 'type': 'Model', 'model': Site }]) @staticmethod def jobStats(site): """ Returns the job status """ return { 'running': BaseJob.objects.filter(site=site).count(), 'error': BaseJob.objects.filter(site=site, state__in=('error', 'aborted', 'paused')).count() } @cinp.action(return_type={'type': 'Map'}) def jobRunnerVariables(self): """ Returns variables internal to the job script """ result = {} runner = pickle.loads(self.script_runner) for module in runner.value_map: for name in runner.value_map[module]: result['{0}.{1}'.format(module, name)] = str( runner.value_map[module][name][0]()) result.update(runner.variable_map) return result @cinp.action(return_type={'type': 'Map'}) def jobRunnerState(self): """ Returns the state of the job script """ result = {} runner = pickle.loads(self.script_runner) blueprint = None try: blueprint = self.foundationjob.foundation.blueprint except ObjectDoesNotExist: pass try: blueprint = self.structurejob.structure.blueprint except ObjectDoesNotExist: pass try: dependency = self.dependencyjob.dependency if dependency.script_structure is not None: blueprint = dependency.script_structure.blueprint else: blueprint = dependency.structure.blueprint except ObjectDoesNotExist: pass if blueprint is not None: result['script'] = blueprint.get_script(self.script_name) result['cur_line'] = runner.cur_line result['state'] = runner.state return result @cinp.check_auth() @staticmethod def checkAuth(user, verb, id_list, action=None): if verb == 'DESCRIBE': return True if verb == 'CALL': return True return False def clean( self, *args, **kwargs ): # TODO: also need to make sure a Structure is in only one complex super().clean(*args, **kwargs) errors = {} if self.state not in self.JOB_STATE_CHOICES: errors['state'] = 'Invalid state "{0}"'.format(self.state) if errors: raise ValidationError(errors) def __str__(self): return 'BaseJob #{0} in "{1}"'.format(self.pk, self.site.pk)
class Foundation( models.Model ): site = models.ForeignKey( Site, on_delete=models.PROTECT ) # ie where to build it blueprint = models.ForeignKey( FoundationBluePrint, on_delete=models.PROTECT ) locator = models.CharField( max_length=100, unique=True ) config_values = MapField( blank=True ) id_map = JSONField( blank=True ) # ie a dict of asset, chassis, system, etc types interfaces = models.ManyToManyField( RealNetworkInterface, through='FoundationNetworkInterface' ) located_at = models.DateTimeField( editable=False, blank=True, null=True ) built_at = models.DateTimeField( editable=False, blank=True, null=True ) updated = models.DateTimeField( editable=False, auto_now=True ) created = models.DateTimeField( editable=False, auto_now_add=True ) def setLocated( self ): try: self.structure.setDestroyed() # TODO: this may be a little harsh except Structure.DoesNotExist: pass self.located_at = timezone.now() self.built_at = None self.save() def setBuilt( self ): if self.located_at is None: self.located_at = timezone.now() self.built_at = timezone.now() self.save() def setDestroyed( self ): try: self.structure.setDestroyed() # TODO: this may be a little harsh except Structure.DoesNotExist: pass self.built_at = None self.located_at = None self.save() @staticmethod def getTscriptValues( write_mode=False ): # locator is handled seperatly return { # none of these base items are writeable, ignore the write_mode for now 'id': ( lambda foundation: foundation.pk, None ), 'type': ( lambda foundation: foundation.subclass.type, None ), 'site': ( lambda foundation: foundation.site.pk, None ), 'blueprint': ( lambda foundation: foundation.blueprint.pk, None ), 'id_map': ( lambda foundation: foundation.ip_map, None ), 'interface_list': ( lambda foundation: [ i for i in foundation.interfaces.all() ], None ) } @staticmethod def getTscriptFunctions(): return {} def configValues( self ): return { 'foundation_id': self.pk, 'foundation_type': self.type, 'foundation_state': self.state, 'foundation_class_list': self.class_list } @property def subclass( self ): for attr in FOUNDATION_SUBCLASS_LIST: try: return getattr( self, attr ) except AttributeError: pass return self @cinp.action( 'String' ) def getRealFoundationURI( self ): # TODO: this is such a hack, figure out a better way subclass = self.subclass class_name = type( subclass ).__name__ if class_name == 'Foundation': return '/api/v1/Building/Foundation:{0}:'.format( subclass.pk ) elif class_name == 'VirtualBoxFoundation': return '/api/v1/VirtualBox/VirtualBoxFoundation:{0}:'.format( subclass.pk ) elif class_name == 'ManualFoundation': return '/api/v1/Manual/ManualFoundation:{0}:'.format( subclass.pk ) raise ValueError( 'Unknown Foundation class "{0}"'.format( class_name ) ) @cinp.action( 'Map' ) def getConfig( self ): return getConfig( self.subclass ) @property def type( self ): return 'Unknown' @property def class_list( self ): # top level generic classes: Metal, VM, Container, Switch, PDU return [] @property def can_auto_locate( self ): # child models can decide if it can auto submit job for building, ie: vm (and like foundations) are only canBuild if their structure is auto_build return False @property def state( self ): if self.located_at is not None and self.built_at is not None: return 'built' elif self.located_at is not None: return 'located' return 'planned' @cinp.list_filter( name='site', paramater_type_list=[ { 'type': 'Model', 'model': 'contractor.Site.models.Site' } ] ) @staticmethod def filter_site( site ): return Foundation.objects.filter( site=site ) @cinp.check_auth() @staticmethod def checkAuth( user, method, id_list, action=None ): return True def clean( self, *args, **kwargs ): super().clean( *args, **kwargs ) errors = {} if self.type not in self.blueprint.foundation_type_list: errors[ 'name' ] = 'Blueprint "{0}" does not list this type ({1})'.format( self.blueprint.description, self.type ) if errors: raise ValidationError( errors ) def __str__( self ): return 'Foundation #{0} of "{1}" in "{2}"'.format( self.pk, self.blueprint.pk, self.site.pk )