diff --git a/awx/main/migrations/0022_v14_changes.py b/awx/main/migrations/0022_v14_changes.py index 5de66caead..0a9568c8a2 100644 --- a/awx/main/migrations/0022_v14_changes.py +++ b/awx/main/migrations/0022_v14_changes.py @@ -118,6 +118,8 @@ class Migration(DataMigration): team.name = team_name team.save() + # FIXME: Check backwards migrations!!! + # Change credential password back to ssh_password. for credential in orm.Credential.objects.all(): credential.ssh_username = credential.username diff --git a/awx/main/migrations/0023_v14_changes.py b/awx/main/migrations/0023_v14_changes.py index f90284b40d..0173c0f9c6 100644 --- a/awx/main/migrations/0023_v14_changes.py +++ b/awx/main/migrations/0023_v14_changes.py @@ -29,6 +29,9 @@ class Migration(SchemaMigration): # Deleting field 'Project.scm_username' db.delete_column(u'main_project', 'scm_username') + # Deleting field 'InventorySource.source_tags' + db.delete_column(u'main_inventorysource', 'source_tags') + # Deleting field 'InventorySource.source_password' db.delete_column(u'main_inventorysource', 'source_password') @@ -70,6 +73,11 @@ class Migration(SchemaMigration): self.gf('django.db.models.fields.CharField')(default='', max_length=256, null=True, blank=True), keep_default=False) + # Adding field 'InventorySource.source_tags' + db.add_column(u'main_inventorysource', 'source_tags', + self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True), + keep_default=False) + # Adding field 'InventorySource.source_password' db.add_column(u'main_inventorysource', 'source_password', self.gf('django.db.models.fields.CharField')(default='', max_length=1024, blank=True), @@ -231,7 +239,6 @@ class Migration(SchemaMigration): 'source': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), 'source_path': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), 'source_regions': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), - 'source_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), 'source_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), 'status': ('django.db.models.fields.CharField', [], {'default': "'none'", 'max_length': '32', 'null': 'True'}), 'update_interval': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index 931039e800..ec823d1add 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -299,7 +299,7 @@ class CommonTask(PrimordialModel): def start(self, **kwargs): task_class = self._get_task_class() - needed = self._get_passwords_needed_to_start + needed = self._get_passwords_needed_to_start() opts = dict([(field, kwargs.get(field, '')) for field in needed]) if not all(opts.values()): return False @@ -320,6 +320,7 @@ class CommonTask(PrimordialModel): return bool(self.status in ('pending', 'waiting', 'running')) def cancel(self): + # FIXME: Force cancel! if self.can_cancel: if not self.cancel_flag: self.cancel_flag = True @@ -822,27 +823,11 @@ class InventorySource(PrimordialModel): default=None, blank=True, ) - #source_username = models.CharField( # FIXME: Remove after migration - # max_length=1024, - # blank=True, - # default='', - #) - #source_password = models.CharField( # FIXME: Remove after migration - # max_length=1024, - # blank=True, - # default='', - #) source_regions = models.CharField( max_length=1024, blank=True, default='', ) - # FIXME: Remove tags field when making other migrations for credential changes! - source_tags = models.CharField( - max_length=1024, - blank=True, - default='', - ) overwrite = models.BooleanField( default=False, help_text=_('Overwrite local groups and hosts from remote inventory source.'), @@ -894,21 +879,6 @@ class InventorySource(PrimordialModel): # If update_fields has been specified, add our field names to it, # if it hasn't been specified, then we're just doing a normal save. update_fields = kwargs.get('update_fields', []) - # When first saving to the database, don't store any password field - # values, but instead save them until after the instance is created. - #if new_instance: - # for field in self.PASSWORD_FIELDS: - # value = getattr(self, field, '') - # setattr(self, '_saved_%s' % field, value) - # setattr(self, field, '') - # Otherwise, store encrypted values to the database. - #else: - # for field in self.PASSWORD_FIELDS: - # encrypted = encrypt_field(self, field, True) - # if getattr(self, field) != encrypted: - # setattr(self, field, encrypted) - # if field not in update_fields: - # update_fields.append(field) # Update status and last_updated fields. updated_fields = self.set_status_and_last_updated(save=False) for field in updated_fields: @@ -921,32 +891,9 @@ class InventorySource(PrimordialModel): update_fields.append('inventory') # Do the actual save. super(InventorySource, self).save(*args, **kwargs) - # After saving a new instance for the first time (to get a primary - # key), set the password fields and save again. - #if new_instance: - # update_fields=[] - # for field in self.PASSWORD_FIELDS: - # saved_value = getattr(self, '_saved_%s' % field, '') - # if getattr(self, field) != saved_value: - # setattr(self, field, saved_value) - # update_fields.append(field) - # if update_fields: - # self.save(update_fields=update_fields) source_vars_dict = VarsDictProperty('source_vars') - @property - def needs_source_password(self): - return self.source and self.source_password == 'ASK' - - @property - def source_passwords_needed(self): - needed = [] - for field in ('source_password',): - if getattr(self, 'needs_%s' % field): - needed.append(field) - return needed - def set_status_and_last_updated(self, save=True): # Determine current status. if self.source: @@ -983,12 +930,8 @@ class InventorySource(PrimordialModel): def update(self, **kwargs): if self.can_update: - needed = self.source_passwords_needed - opts = dict([(field, kwargs.get(field, '')) for field in needed]) - if not all(opts.values()): - return inventory_update = self.inventory_updates.create() - inventory_update.start(**opts) + inventory_update.start() return inventory_update def get_absolute_url(self): @@ -1032,9 +975,6 @@ class InventoryUpdate(CommonTask): from awx.main.tasks import RunInventoryUpdate return RunInventoryUpdate - def _get_passwords_needed_to_start(self): - return self.inventory_source.source_passwords_needed - class Credential(CommonModelNameNotUnique): ''' @@ -1077,14 +1017,6 @@ class Credential(CommonModelNameNotUnique): choices=KIND_CHOICES, default='ssh', ) - - #ssh_username = models.CharField( - # blank=True, - # default='', - # max_length=1024, - # verbose_name=_('SSH username'), - # help_text=_('SSH username for a job using this credential.'), - #) username = models.CharField( blank=True, default='', @@ -1092,13 +1024,6 @@ class Credential(CommonModelNameNotUnique): verbose_name=_('Username'), help_text=_('Username for this credential.'), ) - #ssh_password = models.CharField( - # blank=True, - # default='', - # max_length=1024, - # verbose_name=_('SSH password'), - # help_text=_('SSH password (or "ASK" to prompt the user).'), - #) password = models.CharField( blank=True, default='', @@ -1217,8 +1142,6 @@ class Project(CommonModel): ('successful', 'Successful'), ] - #PASSWORD_FIELDS = ('scm_password', 'scm_key_data', 'scm_key_unlock') - SCM_TYPE_CHOICES = [ ('', _('Manual')), ('git', _('Git')), @@ -1296,38 +1219,6 @@ class Project(CommonModel): null=True, default=None, ) - #scm_username = models.CharField( - # blank=True, - # null=True, - # default='', - # max_length=256, - # verbose_name=_('Username'), - # help_text=_('SCM username for this project.'), - #) - #scm_password = models.CharField( - # blank=True, - # null=True, - # default='', - # max_length=1024, - # verbose_name=_('Password'), - # help_text=_('SCM password (or "ASK" to prompt the user).'), - #) - #scm_key_data = models.TextField( - # blank=True, - # null=True, - # default='', - # verbose_name=_('SSH private key'), - # help_text=_('RSA or DSA private key to be used instead of password.'), - #) - #scm_key_unlock = models.CharField( - # max_length=1024, - # null=True, - # blank=True, - # default='', - # verbose_name=_('SSH key unlock'), - # help_text=_('Passphrase to unlock SSH private key if encrypted (or ' - # '"ASK" to prompt the user).'), - #) current_update = models.ForeignKey( 'ProjectUpdate', null=True, @@ -1364,21 +1255,6 @@ class Project(CommonModel): # If update_fields has been specified, add our field names to it, # if it hasn't been specified, then we're just doing a normal save. update_fields = kwargs.get('update_fields', []) - # When first saving to the database, don't store any password field - # values, but instead save them until after the instance is created. - #if new_instance: - # for field in self.PASSWORD_FIELDS: - # value = getattr(self, field, '') - # setattr(self, '_saved_%s' % field, value) - # setattr(self, field, '') - # Otherwise, store encrypted values to the database. - #else: - # for field in self.PASSWORD_FIELDS: - # encrypted = encrypt_field(self, field, bool(field != 'scm_key_data')) - # if getattr(self, field) != encrypted: - # setattr(self, field, encrypted) - # if field not in update_fields: - # update_fields.append(field) # Check if scm_type or scm_url changes. if self.pk: project_before = Project.objects.get(pk=self.pk) @@ -1399,18 +1275,11 @@ class Project(CommonModel): update_fields.append(field) # Do the actual save. super(Project, self).save(*args, **kwargs) - # After saving a new instance for the first time (to get a primary - # key), set the password fields and save again. if new_instance: update_fields=[] # Generate local_path for SCM after initial save (so we have a PK). if self.scm_type and not self.local_path.startswith('_'): update_fields.append('local_path') - # for field in self.PASSWORD_FIELDS: - # saved_value = getattr(self, '_saved_%s' % field, '') - # if getattr(self, field) != saved_value: - # setattr(self, field, saved_value) - # update_fields.append(field) if update_fields: self.save(update_fields=update_fields) # If we just created a new project with SCM and it doesn't require any @@ -1420,14 +1289,11 @@ class Project(CommonModel): @property def needs_scm_password(self): - return self.scm_type and not self.scm_key_data and \ - self.scm_password == 'ASK' + return self.credential and self.credential.needs_password @property def needs_scm_key_unlock(self): - return self.scm_type and self.scm_key_data and \ - 'ENCRYPTED' in decrypt_field(self, 'scm_key_data') and \ - (not self.scm_key_unlock or self.scm_key_unlock == 'ASK') + return self.credential and self.credential.needs_ssh_key_unlock @property def scm_passwords_needed(self): @@ -1690,11 +1556,6 @@ class JobTemplate(CommonModel): ''' save_job = kwargs.pop('save', True) kwargs['job_template'] = self - # Create new name with timestamp format to match jobs launched by the UI. - new_name = '%s %s' % (self.name, now().strftime('%Y-%m-%dT%H:%M:%S.%fZ')) - new_name = new_name[:-4] + 'Z' - kwargs.setdefault('name', new_name) - kwargs.setdefault('description', self.description) kwargs.setdefault('job_type', self.job_type) kwargs.setdefault('inventory', self.inventory) kwargs.setdefault('project', self.project) diff --git a/awx/main/serializers.py b/awx/main/serializers.py index 8dc57b79ca..f10cebc762 100644 --- a/awx/main/serializers.py +++ b/awx/main/serializers.py @@ -287,10 +287,6 @@ class OrganizationSerializer(BaseSerializer): class ProjectSerializer(BaseSerializer): - #scm_password = serializers.WritableField(required=False, default='') - #scm_key_data = serializers.WritableField(required=False, default='') - #scm_key_unlock = serializers.WritableField(required=False, default='') - playbooks = serializers.Field(source='playbooks', help_text='Array of playbooks available within this project.') scm_delete_on_next_update = serializers.Field(source='scm_delete_on_next_update') @@ -300,26 +296,8 @@ class ProjectSerializer(BaseSerializer): 'scm_branch', 'scm_clean', 'scm_delete_on_update', 'scm_delete_on_next_update', 'scm_update_on_launch', 'credential', - #'scm_username', 'scm_password', 'scm_key_data', - #'scm_key_unlock', 'last_update_failed', 'status', 'last_updated') - #def to_native(self, obj): - #ret = super(ProjectSerializer, self).to_native(obj) - # Replace the actual encrypted value with the string $encrypted$. - #for field in Project.PASSWORD_FIELDS: - # if field in ret and unicode(ret[field]).startswith('$encrypted$'): - # ret[field] = '$encrypted$' - #return ret - - #def restore_object(self, attrs, instance=None): - # # If the value sent to the API startswith $encrypted$, ignore it. - # for field in Project.PASSWORD_FIELDS: - # if unicode(attrs.get(field, '')).startswith('$encrypted$'): - # attrs.pop(field, None) - # instance = super(ProjectSerializer, self).restore_object(attrs, instance) - # return instance - def get_related(self, obj): if obj is None: return {} @@ -677,39 +655,11 @@ class InventorySourceSerializer(BaseSerializer): model = InventorySource fields = ('id', 'url', 'related', 'summary_fields', 'created', 'modified', 'inventory', 'group', 'source', 'source_path', - 'source_vars', 'credential', - # 'source_username', 'source_password', - 'source_regions', 'overwrite', 'overwrite_vars', - 'update_on_launch', 'update_interval', 'last_update_failed', - 'status', 'last_updated') + 'source_vars', 'credential', 'source_regions', 'overwrite', + 'overwrite_vars', 'update_on_launch', 'update_interval', + 'last_update_failed', 'status', 'last_updated') read_only_fields = ('inventory', 'group') - def to_native(self, obj): - ret = super(InventorySourceSerializer, self).to_native(obj) - # Replace the actual encrypted value with the string $encrypted$. - #for field in InventorySource.PASSWORD_FIELDS: - # if field in ret and unicode(ret[field]).startswith('$encrypted$'): - # ret[field] = '$encrypted$' - # Make regions/tags into a list of strings. - #for field in ('source_regions', 'source_tags'): - # if field in ret: - # value = ret[field] - # if isinstance(value, basestring): - # ret[field] = [x.strip() for x in value.split(',') if x.strip()] - return ret - - def restore_object(self, attrs, instance=None): - # If the value sent to the API startswith $encrypted$, ignore it. - #for field in InventorySource.PASSWORD_FIELDS: - # if unicode(attrs.get(field, '')).startswith('$encrypted$'): - # attrs.pop(field, None) - #for field in ('source_regions', 'source_tags'): - # value = attrs.get(field, []) - # if isinstance(value, (list,tuple)): - # attrs[field] = ','.join([unicode(x).strip() for x in value]) - instance = super(InventorySourceSerializer, self).restore_object(attrs, instance) - return instance - def get_related(self, obj): if obj is None: return {} @@ -855,21 +805,21 @@ class CredentialSerializer(BaseSerializer): 'ssh_key_unlock', 'sudo_username', 'sudo_password', 'user', 'team',) - #def to_native(self, obj): - # ret = super(CredentialSerializer, self).to_native(obj) - # # Replace the actual encrypted value with the string $encrypted$. - # for field in Credential.PASSWORD_FIELDS: - # if field in ret and unicode(ret[field]).startswith('$encrypted$'): - # ret[field] = '$encrypted$' - # return ret + def to_native(self, obj): + ret = super(CredentialSerializer, self).to_native(obj) + # Replace the actual encrypted value with the string $encrypted$. + for field in Credential.PASSWORD_FIELDS: + if field in ret and unicode(ret[field]).startswith('$encrypted$'): + ret[field] = '$encrypted$' + return ret - #def restore_object(self, attrs, instance=None): - # # If the value sent to the API startswith $encrypted$, ignore it. - # for field in Credential.PASSWORD_FIELDS: - # if unicode(attrs.get(field, '')).startswith('$encrypted$'): - # attrs.pop(field, None) - # instance = super(CredentialSerializer, self).restore_object(attrs, instance) - # return instance + def restore_object(self, attrs, instance=None): + # If the value sent to the API startswith $encrypted$, ignore it. + for field in Credential.PASSWORD_FIELDS: + if unicode(attrs.get(field, '')).startswith('$encrypted$'): + attrs.pop(field, None) + instance = super(CredentialSerializer, self).restore_object(attrs, instance) + return instance def get_related(self, obj): if obj is None: diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 9fc963e7e3..bc8c82afca 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -301,7 +301,7 @@ class RunJob(BaseTask): passwords = super(RunJob, self).build_passwords(job, **kwargs) creds = job.credential if creds: - for field in ('ssh_key_unlock', 'ssh_password', 'sudo_password'): + for field in ('ssh_key_unlock', 'password', 'sudo_password'): value = kwargs.get(field, decrypt_field(creds, field)) if value not in ('', 'ASK'): passwords[field] = value @@ -340,7 +340,7 @@ class RunJob(BaseTask): creds = job.credential ssh_username, sudo_username = '', '' if creds: - ssh_username = kwargs.get('ssh_username', creds.ssh_username) + ssh_username = kwargs.get('username', creds.username) sudo_username = kwargs.get('sudo_username', creds.sudo_username) # Always specify the normal SSH user as root by default. Since this # task is normally running in the background under a service account, @@ -392,8 +392,8 @@ class RunJob(BaseTask): d = super(RunJob, self).get_password_prompts() d.update({ r'sudo password.*:': 'sudo_password', - r'SSH password:': 'ssh_password', - r'Password:': 'ssh_password', + r'SSH password:': 'password', + r'Password:': 'password', }) return d @@ -509,7 +509,8 @@ class RunProjectUpdate(BaseTask): Return SSH private key data needed for this project update. ''' project = project_update.project - return decrypt_field(project, 'scm_key_data') or None + if project.credential: + return decrypt_field(project.credential, 'ssh_key_data') or None def build_passwords(self, project_update, **kwargs): ''' @@ -519,12 +520,13 @@ class RunProjectUpdate(BaseTask): passwords = super(RunProjectUpdate, self).build_passwords(project_update, **kwargs) project = project_update.project - value = kwargs.get('scm_key_unlock', decrypt_field(project, 'scm_key_unlock')) - if value not in ('', 'ASK'): - passwords['ssh_key_unlock'] = value - passwords['scm_username'] = project.scm_username - passwords['scm_password'] = kwargs.get('scm_password', - decrypt_field(project, 'scm_password')) + if project.credential: + value = kwargs.get('scm_key_unlock', decrypt_field(project.credential, 'ssh_key_unlock')) + if value not in ('', 'ASK'): + passwords['scm_key_unlock'] = value + passwords['scm_username'] = project.scm_username + passwords['scm_password'] = kwargs.get('scm_password', + decrypt_field(project.credential, 'password')) return passwords def build_env(self, project_update, **kwargs): @@ -547,9 +549,9 @@ class RunProjectUpdate(BaseTask): scm_type = project.scm_type scm_url = update_scm_url(scm_type, project.scm_url) scm_url_parts = urlparse.urlsplit(scm_url) - scm_username = kwargs.get('passwords', {}).get('scm_username', '') + scm_username = kwargs.get('passwords', {}).get('username', '') scm_username = scm_username or scm_url_parts.username or '' - scm_password = kwargs.get('passwords', {}).get('scm_password', '') + scm_password = kwargs.get('passwords', {}).get('password', '') scm_password = scm_password or scm_url_parts.password or '' if scm_username and scm_password not in ('ASK', ''): if scm_type == 'svn': @@ -731,9 +733,11 @@ class RunInventoryUpdate(BaseTask): elif inventory_source.source == 'rackspace': section = 'rackspace_cloud' cp.add_section(section) - cp.set(section, 'username', inventory_source.source_username) - cp.set(section, 'api_key', decrypt_field(inventory_source, - 'source_password')) + credential = inventory_source.credential + if credential: + cp.set(section, 'username', credential.username) + cp.set(section, 'api_key', decrypt_field(credential, + 'password')) # Return INI content. if cp.sections(): f = cStringIO.StringIO() @@ -747,9 +751,10 @@ class RunInventoryUpdate(BaseTask): passwords = super(RunInventoryUpdate, self).build_passwords(inventory_update, **kwargs) inventory_source = inventory_update.inventory_source - passwords['source_username'] = inventory_source.source_username - passwords['source_password'] = kwargs.get('source_password', \ - decrypt_field(inventory_source, 'source_password')) + credential = inventory_source.credential + if credential: + passwords['source_username'] = credential.username + passwords['source_password'] = decrypt_field(credential, 'password')) return passwords def build_env(self, inventory_update, **kwargs): diff --git a/awx/main/views.py b/awx/main/views.py index b7a8541dec..87f5a877be 100644 --- a/awx/main/views.py +++ b/awx/main/views.py @@ -748,8 +748,6 @@ class InventorySourceUpdateView(GenericAPIView): data = dict( can_update=obj.can_update, ) - if obj.source: - data['passwords_needed_to_update'] = obj.source_passwords_needed return Response(data) def post(self, request, *args, **kwargs): @@ -757,8 +755,7 @@ class InventorySourceUpdateView(GenericAPIView): if obj.can_update: inventory_update = obj.update(**request.DATA) if not inventory_update: - data = dict(passwords_needed_to_update=obj.source_passwords_needed) - return Response(data, status=status.HTTP_400_BAD_REQUEST) + return Response({}, status=status.HTTP_400_BAD_REQUEST) else: headers = {'Location': inventory_update.get_absolute_url()} return Response(status=status.HTTP_202_ACCEPTED, headers=headers)