From 44372ed191444e53c7d7157f311b300a49851ded Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Sun, 24 Mar 2013 15:54:57 -0400 Subject: [PATCH] Start of API guide + some whitespace removal. --- TODO.md | 15 +- docs/api_guide.md | 310 ++++++++++++++++++++++++++++++++++++ lib/main/models/__init__.py | 118 +++++++------- 3 files changed, 374 insertions(+), 69 deletions(-) create mode 100644 docs/api_guide.md diff --git a/TODO.md b/TODO.md index a0f3e964f7..0c70b5fec4 100644 --- a/TODO.md +++ b/TODO.md @@ -6,7 +6,7 @@ TODO items for ansible commander NEXT STEPS * Michael -- flesh out REST model & tests for everything, API guide -* Chris ---- document celery devs stuff. +* Chris ---- document celery devs stuff. * Chris ---- inventory script is done, integrate with celery system, make celery REST triggerable, callback plugin * Michael -- enhance callback plugin to provide runner and playbook context @@ -14,23 +14,18 @@ LATER ----- * UI layer -* CLI client +* CLI client (and libs) * clean up initial migrations * init scripts, Apache proxying, etc * does inventory script need any caching * credentials subsystem -- let app hold on to keys for user -TWEAKS ------- +TWEAKS/ASSORTED +--------------- * add a synthetic bit to the organization to indicate if the current user is an administator of it -* add related resources to the user object to they can quickly find organizations they are an admin of, organizations, teams, projects, etc - -ASSORTED -------- - * uniqueness checks for playbook paths? -* allow multiple playbook execution types per project, different tag choices (Projects/Runnables?) +* allow multiple playbook execution types per project, different --tag choices, different --limit choices (maybe just free form in the job for now?) * permissions infrastructure about who can kick off what kind of jobs diff --git a/docs/api_guide.md b/docs/api_guide.md new file mode 100644 index 0000000000..dd06665aa5 --- /dev/null +++ b/docs/api_guide.md @@ -0,0 +1,310 @@ +API Guide +========= + +This document describes how to do nearly everything in Ansible commander via the REST API. + +To begin, you should have set up Ansible Commander (see SETUP.md) and created at least one Django super user. +To do this from the development project, run "make adduser". + +For the purposes of this guide, we will assume the superuser username and password is admin/admin. + +Conventions +=========== + +* Ansible Commander uses a standard REST API, rooted at /api/v1/ on the server. +* All data is JSON. You may have to specify the content/type on POST or PUT requests accordingly. +* All URIs should end in "/" or you will get a 301 redirect. + +Time to get started. + +Aside: Django Admin +=================== + +When working with REST objects it is often helpful to use the Django admin interface to pre-create objects, +and then retrieve collections of objects with GET request to look at their structure. This will help +users understand how to create new objects as well as how to edit them. + +Aside: Read Only Fields +======================= + +Certain fields in the REST API are marked read only. These usually include the URL of a resource, the ID, +and occasionally some internal fields. For instance, the 'created_by' attribute of each object indicates +which user created the resource, and can not be edited. + +Working With Users +================== + +After a fresh install, there should be no organizations and only the superuser. + +Since we're explaining our first resource, we'll be a bit more verbose with the user +information than with some other REST resources. + +Listing Users +------------- + +We will pay special attention to this first access request, as any list view looks basically +the same. + + curl http://admin:admin@127.0.0.1:8013/api/v1/users/ + + { + "count": 1, "next": null, "previous": null, + "results": [ + { + "url": "/api/v1/users/1/", + "id": 1, "username": "admin", + "first_name": "", + "last_name": "", + "email": "root@localhost.localdomain", + "is_active": true, + "is_superuser": true, + "related": { + "admin_of_organizations": "/api/v1/users/1/admin_of_organizations/", + "organizations": "/api/v1/users/1/organizations/", + "teams": "/api/v1/users/1/teams/" + } + } + ] + } + +Notice that the requests are paginated, so you may have to access additional URLs if 'previous' and 'next' +are set to access the full collection. + +You will also see 'related' URLs on each object, which tell you from any point in the API what resources +you can get to from a particular resource. + +Adding Users +------------ + +To add a user, just POST to the users collection: + + curl -X POST -d @input http://admin:admin@127.0.0.1:8013/api/v1/users/ + + { + "username" : "foo", "password: "foo" + } + +And optionally specify other fields. Super users or organization admins can create users. We'll learn more about +organizations shortly! + +Getting a User +-------------- + +As you would expect, a user, superuser, or organization admin can retrieve the record of a user directly via: + + curl http://admin:admin@127.0.0.1/api/v1/users/1/ + +And recieves something very much like this: + + { + "url": "/api/v1/users/1/", + "id": 1, "username": "admin", + "first_name": "", + "last_name": "", + "email": "root@localhost.localdomain", + "is_active": true, + "is_superuser": true, + "related": { + "admin_of_organizations": "/api/v1/users/1/admin_of_organizations/", + "organizations": "/api/v1/users/1/organizations/", + "teams": "/api/v1/users/1/teams/" + } + } + + +Deleting Users +-------------- + +In ansible commander, nothing is truly deleted, objects are only set inactive and renamed. This allows recovery +of those objects using the Django administrative interface or the database command line. + +To delete an object, just DELETE to a resource, like so: + + curl -X DELETE http://admin:admin@127.0.0.1:8013/api/v1/users/2/ + +Only a user's organization admins, or a superuser, can mark a user as deleted. + +Updating Users +-------------- + +A user may update only his password, and an organization admin or superuser can update a lot more about the +user. To update a user, just PUT to the resource, like so: + + curl -X PUT -d @input http://admin:admin@127.0.0.1:8013/api/v1/users/2/ + + { + 'password': 'new_password' + } + +Organizations +============= + +Organizations are the basic unit of multi-tenancy and access in Ansible Commander. + +At the basic level, when a user is created, he should then be added to one or more organziations. + +These may represent concepts like "Engineering" or "Finance". + +Under each organization, there will also be multiple teams, like "Engineering QA Team", or "Engineering Release Team". + +Organizations also have admin users (which can be assigned by the super user or other organization admins) whom also +have most of the powers of a superuser to users inside that organization. + +Listing Organizations +--------------------- + +GET /api/v1/organizations/ + +Editing Organizations +--------------------- + +PUT /api/v1/organizations/1/ + +Adding users to an organization +------------------------------- + +POST the data you recieved from GET /api/v1/users/X/ to /api/v1/organizations/Y/organizations/ + +Similarly, you can list users of an organization if you are an org admin or superuser with: + +GET /api/v1/organizations/X/users/ + +Adding admins to an organization +-------------------------------- + +POST the data you recieved from GET /api/v1/users/X/ to /api/v1/organizations/Y/admins/ + +Similarly, you can list admins of an organization if you are an org admin or superuser, with: + +GET /api/v1/organizations/X/admins/ + +Note that adding a user as an admin does not automatically add them as a regular organization user. If the user +is to be a member of any project teams, add them both to the admin list and the users list for a given organization. + +Adding projects to an organization +---------------------------------- + +An organization may also have projects, which would be a concept like "ACME Corp Application" or "Application Foo". + +Projects are covered in a later section, but are added to organizations, once created, by posting the record +recieved from /api/v1/projects/X/ to /api/v1/organizations/Y/. + +Removing users, admins, and projects from organizations +------------------------------------------------------- + +Deleting from sub-collections in Ansible Commander is not handled by a DELETE verb, but by a modified POST. + +To remove a user ID X from an organization Y, simply POST the following to the collection /api/v1/organizations/Y/users/: + + { 'id' : X, 'disassociate' : 1 } + +A similar post can also be made to /api/v1/organizations/Y/admins/ to remove an admin user or +/api/v1/organizations/Y/projects/. + +The same removal facility is available elsewhere in Ansible commander for most related resources. + +Projects +======== + +... + +Teams +===== + +... + +Inventories +=========== + +... + +Adding Inventories +------------------ + +Adding Hosts +------------ + +Adding Groups +------------- + +Editing Hosts or Groups +----------------------- + +Adding Host Variables +--------------------- + +Adding Group Variables +---------------------- + +Editing Host or Group Variables +------------------------------- + +Removing Host or Group Variables +-------------------------------- + +Adding Sub Groups +----------------- + +Removing Sub Groups +------------------- + +Access Via Inventory Script +--------------------------- + +Credentials +=========== + +Creating credentials +-------------------- + +Adding credentials to users, teams, or projects +----------------------------------------------- + +Seeing what credentials a user has access to +-------------------------------------------- + +Launch Jobs +=========== + +Submitting A Launch Job +----------------------- + +Getting Job Results +------------------- + +Browsing Log Data +----------------- + + +Tags +==== + +Adding Tags +----------- + +Finding Tagged Resources +------------------------ + +Editing/Removing Tags +--------------------- + +Permissions +=========== + +Permissions can be granted to users or teams and indicate they are allowed to use +certain combinations of credentials, inventories to deploy to certain projects. + +For instance, a permission may define that the QA Team can use certain credentials to push +to a Test Inventory, while another permission may indicate the Release Engineering team can +push a different project with different credentials to a Production inventory source. + +Audit Trails +============ + +Viewing object audit trails +--------------------------- + + + + + diff --git a/lib/main/models/__init__.py b/lib/main/models/__init__.py index 2678fac909..1e7cb05f7b 100644 --- a/lib/main/models/__init__.py +++ b/lib/main/models/__init__.py @@ -37,10 +37,10 @@ class EditHelper(object): 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'): + 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: + if len(changed.keys()) > 0: return True return False @@ -49,7 +49,7 @@ class EditHelper(object): ''' return the fields that would be changed by a prospective PUT operation ''' changed = {} for f in fields: - left = getattr(obj, f, None) + 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) @@ -74,11 +74,11 @@ class UserHelper(object): @classmethod def can_user_read(cls, user, obj): - ''' a user can be read if they are on the same team or can be administrated ''' + ''' 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) - - @classmethod + + @classmethod def can_user_delete(cls, user, obj): if user.is_superuser: return True @@ -87,8 +87,8 @@ class UserHelper(object): class CommonModel(models.Model): - ''' - common model for all object types that have these standard fields + ''' + common model for all object types that have these standard fields ''' class Meta: @@ -98,23 +98,23 @@ class CommonModel(models.Model): description = models.TextField(blank=True, default='') 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! creation_date = models.DateField(auto_now_add=True) - tags = models.ManyToManyField('Tag', related_name='%(class)s_by_tag', blank=True) + 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) active = models.BooleanField(default=True) def __unicode__(self): return unicode(self.name) - @classmethod + @classmethod def can_user_administrate(cls, user, obj): # FIXME: do we want a seperate method to override put? This is kind of general purpose raise exceptions.NotImplementedError() - @classmethod + @classmethod def can_user_delete(cls, user, obj): raise exceptions.NotImplementedError() - @classmethod + @classmethod def can_user_read(cls, user, obj): raise exceptions.NotImplementedError() @@ -130,16 +130,16 @@ class CommonModel(models.Model): return False rc = cls.can_user_administrate(user, obj) return rc - + @classmethod def can_user_unattach(cls, user, obj, sub_obj, relationship): return cls.can_user_administrate(user, obj) - + class Tag(models.Model): - ''' - any type of object can be given a search tag ''' - + any type of object can be given a search tag + ''' + class Meta: app_label = 'main' @@ -162,15 +162,15 @@ class Tag(models.Model): # anybody can read tags, we won't show much detail other than the names return True - + class AuditTrail(models.Model): - ''' - changing any object records the change ''' - + changing any object records the change + ''' + class Meta: app_label = 'main' - + resource_type = models.CharField(max_length=64) modified_by = models.ForeignKey('auth.User', on_delete=SET_NULL, null=True, blank=True) delta = models.TextField() # FIXME: switch to JSONField @@ -181,10 +181,10 @@ class AuditTrail(models.Model): tag = models.ForeignKey('Tag', on_delete=SET_NULL, null=True, blank=True) class Organization(CommonModel): - ''' - organizations are the basic unit of multi-tenancy divisions - ''' - + ''' + organizations are the basic unit of multi-tenancy divisions + ''' + class Meta: app_label = 'main' @@ -196,11 +196,11 @@ class Organization(CommonModel): import lib.urls return reverse(lib.urls.views_OrganizationsDetail, args=(self.pk,)) - @classmethod + @classmethod def can_user_delete(cls, user, obj): return user in obj.admins.all() - @classmethod + @classmethod def can_user_administrate(cls, user, obj): # FIXME: super user checks should be higher up so we don't have to repeat them if user.is_superuser: @@ -210,11 +210,11 @@ class Organization(CommonModel): rc = user in obj.admins.all() return rc - @classmethod + @classmethod def can_user_read(cls, user, obj): return cls.can_user_administrate(user,obj) or user in obj.users.all() - @classmethod + @classmethod def can_user_delete(cls, user, obj): return cls.can_user_administrate(user, obj) @@ -222,15 +222,15 @@ class Organization(CommonModel): return self.name class Inventory(CommonModel): - ''' + ''' an inventory source contains lists and hosts. ''' - + class Meta: app_label = 'main' verbose_name_plural = _('inventories') - - organization = models.ForeignKey(Organization, null=True, on_delete=SET_NULL, related_name='inventories') + + organization = models.ForeignKey(Organization, null=True, on_delete=SET_NULL, related_name='inventories') def __unicode__(self): if self.organization: @@ -242,7 +242,7 @@ class Host(CommonModel): ''' A managed node ''' - + class Meta: app_label = 'main' @@ -255,7 +255,7 @@ class Group(CommonModel): ''' A group of managed nodes. May belong to multiple groups ''' - + class Meta: app_label = 'main' @@ -272,8 +272,8 @@ class Group(CommonModel): class VariableData(CommonModel): ''' A set of host or group variables - ''' - + ''' + class Meta: app_label = 'main' verbose_name_plural = _('variable data') @@ -291,7 +291,7 @@ class Credential(CommonModel): Usually this is a SSH key location, and possibly an unlock password. If used with sudo, a sudo password should be set if required. ''' - + class Meta: app_label = 'main' @@ -304,25 +304,25 @@ class Credential(CommonModel): 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) - + class Team(CommonModel): ''' A team is a group of users that work on common projects. ''' - + class Meta: app_label = 'main' - + projects = models.ManyToManyField('Project', blank=True, related_name='teams') users = models.ManyToManyField('auth.User', blank=True, related_name='teams') organizations = models.ManyToManyField('Organization', related_name='teams') class Project(CommonModel): - ''' + ''' A project represents a playbook git repo that can access a set of inventories - ''' - + ''' + inventories = models.ManyToManyField('Inventory', blank=True, related_name='projects') local_repository = models.CharField(max_length=1024) scm_type = models.CharField(max_length=64) @@ -332,7 +332,7 @@ class Project(CommonModel): import lib.urls return reverse(lib.urls.views_ProjectsDetail, args=(self.pk,)) - @classmethod + @classmethod def can_user_administrate(cls, user, obj): if user.is_superuser: return True @@ -357,7 +357,7 @@ class Permission(CommonModel): ''' A permission allows a user, project, or team to be able to use an inventory source. ''' - + class Meta: app_label = 'main' @@ -369,10 +369,10 @@ class Permission(CommonModel): # TODO: other job types (later) class LaunchJob(CommonModel): - ''' - a launch job is a request to apply a project to an inventory source with a given credential ''' - + a launch job is a request to apply a project to an inventory source with a given credential + ''' + class Meta: app_label = 'main' @@ -387,18 +387,18 @@ class LaunchJob(CommonModel): return run_launch_job.delay(self.pk) # project has one default playbook but really should have a list of playbooks and flags ... - - - # ENOUGH_TO_RUN_DJANGO=foo ACOM_INVENTORY_ID= ansible-playbook -i ansible-commander-inventory.py + + + # ENOUGH_TO_RUN_DJANGO=foo ACOM_INVENTORY_ID= ansible-playbook -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 - + # + # playbook in source control is already on the disk + # job_type: # run, check -- enough for now, more initially # if check, add "--check" to parameters @@ -408,7 +408,7 @@ class LaunchJob(CommonModel): # 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 @@ -423,7 +423,7 @@ class LaunchJob(CommonModel): # TODO: Events class LaunchJobStatus(CommonModel): - + class Meta: app_label = 'main' verbose_name_plural = _('launch job statuses') @@ -431,7 +431,7 @@ class LaunchJobStatus(CommonModel): launch_job = models.ForeignKey('LaunchJob', null=True, on_delete=SET_NULL, related_name='launch_job_statuses') status = models.IntegerField() result_data = models.TextField() - + # TODO: reporting (MPD)