Update browsable API built-in documentation to use templates.

This commit is contained in:
Chris Church 2013-08-04 20:46:26 -04:00
parent 538097ed8e
commit 057e7bad59
36 changed files with 606 additions and 252 deletions

View File

@ -2,12 +2,14 @@
# All Rights Reserved.
# Python
import inspect
import json
# Django
from django.http import HttpResponse, Http404
from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404
from django.template.loader import render_to_string
from django.utils.timezone import now
# Django REST Framework
@ -15,31 +17,71 @@ from rest_framework.exceptions import PermissionDenied
from rest_framework import generics
from rest_framework.response import Response
from rest_framework import status
from rest_framework import views
# AWX
from awx.main.models import *
from awx.main.utils import *
# FIXME: machinery for auto-adding audit trail logs to all CREATE/EDITS
class ListAPIView(generics.ListAPIView):
# Base class for a read-only list view.
__all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'ListCreateAPIView',
'SubListAPIView', 'SubListCreateAPIView', 'RetrieveAPIView',
'RetrieveUpdateAPIView', 'RetrieveUpdateDestroyAPIView']
class APIView(views.APIView):
def get_description_context(self):
return {
'docstring': type(self).__doc__ or '',
}
def get_description(self, html=False):
template_list = []
for klass in inspect.getmro(type(self)):
template_basename = camelcase_to_underscore(klass.__name__)
template_list.append('main/%s.md' % template_basename)
context = self.get_description_context()
return render_to_string(template_list, context)
class GenericAPIView(generics.GenericAPIView, APIView):
# Base class for all model-based views.
# Subclasses should define:
# model = ModelClass
# serializer_class = SerializerClass
def get_queryset(self):
return self.request.user.get_queryset(self.model)
def get_description_vars(self):
return {
def get_description_context(self):
# Set instance attributes needed to get serializer metadata.
if not hasattr(self, 'request'):
self.request = None
if not hasattr(self, 'format_kwarg'):
self.format_kwarg = 'format'
d = super(GenericAPIView, self).get_description_context()
d.update({
'model_verbose_name': unicode(self.model._meta.verbose_name),
'model_verbose_name_plural': unicode(self.model._meta.verbose_name_plural),
}
'serializer_fields': self.get_serializer().metadata(),
})
return d
def get_description(self, html=False):
s = 'Use a GET request to retrieve a list of %(model_verbose_name_plural)s.'
return s % self.get_description_vars()
class ListAPIView(generics.ListAPIView, GenericAPIView):
# Base class for a read-only list view.
def get_description_context(self):
opts = self.model._meta
if 'username' in opts.get_all_field_names():
order_field = 'username'
else:
order_field = 'name'
d = super(ListAPIView, self).get_description_context()
d.update({
'order_field': order_field,
})
return d
def get_queryset(self):
return self.request.user.get_queryset(self.model)
class ListCreateAPIView(ListAPIView, generics.ListCreateAPIView):
# Base class for a list view that allows creating new objects.
@ -49,11 +91,6 @@ class ListCreateAPIView(ListAPIView, generics.ListCreateAPIView):
if isinstance(obj, PrimordialModel):
obj.created_by = self.request.user
def get_description(self, html=False):
s = 'Use a GET request to retrieve a list of %(model_verbose_name_plural)s.'
s2 = 'Use a POST request with required %(model_verbose_name)s fields to create a new %(model_verbose_name)s.'
return '\n\n'.join([s, s2]) % self.get_description_vars()
class SubListAPIView(ListAPIView):
# Base class for a read-only sublist view.
@ -66,18 +103,14 @@ class SubListAPIView(ListAPIView):
# to view sublist):
# parent_access = 'read'
def get_description_vars(self):
d = super(SubListAPIView, self).get_description_vars()
def get_description_context(self):
d = super(SubListAPIView, self).get_description_context()
d.update({
'parent_model_verbose_name': unicode(self.parent_model._meta.verbose_name),
'parent_model_verbose_name_plural': unicode(self.parent_model._meta.verbose_name_plural),
})
return d
def get_description(self, html=False):
s = 'Use a GET request to retrieve a list of %(model_verbose_name_plural)s associated with the selected %(parent_model_verbose_name)s.'
return s % self.get_description_vars()
def get_parent_object(self):
parent_filter = {
self.lookup_field: self.kwargs.get(self.lookup_field, None),
@ -109,16 +142,12 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
# sub_obj requires a foreign key to the parent):
# parent_key = 'field_on_model_referring_to_parent'
def get_description(self, html=False):
s = 'Use a GET request to retrieve a list of %(model_verbose_name_plural)s associated with the selected %(parent_model_verbose_name)s.'
s2 = 'Use a POST request with required %(model_verbose_name)s fields to create a new %(model_verbose_name)s.'
if getattr(self, 'parent_key', None):
s3 = 'Use a POST request with an `id` field and `disassociate` set to delete the associated %(model_verbose_name)s.'
s4 = ''
else:
s3 = 'Use a POST request with only an `id` field to associate an existing %(model_verbose_name)s with this %(parent_model_verbose_name)s.'
s4 = 'Use a POST request with an `id` field and `disassociate` set to remove the %(model_verbose_name)s from this %(parent_model_verbose_name)s without deleting the %(model_verbose_name)s.'
return '\n\n'.join(filter(None, [s, s2, s3, s4])) % self.get_description_vars()
def get_description_context(self):
d = super(SubListCreateAPIView, self).get_description_context()
d.update({
'parent_key': getattr(self, 'parent_key', None),
})
return d
def create(self, request, *args, **kwargs):
# If the object ID was not specified, it probably doesn't exist in the
@ -148,7 +177,8 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
if not request.user.can_access(self.model, 'add', serializer.init_data):
raise PermissionDenied()
# save the object through the serializer, reload and returned the saved object deserialized
# save the object through the serializer, reload and returned the saved
# object deserialized
obj = serializer.save()
serializer = self.serializer_class(obj)
@ -168,17 +198,19 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
return response
sub_id = response.data['id']
data = response.data
try:
location = response['Location']
except KeyError:
location = None
created = True
# Retrive the sub object (whether created or by ID).
try:
sub = self.model.objects.get(pk=sub_id)
except self.model.DoesNotExist:
data = dict(msg='Object with id %s cannot be found' % sub_id)
return Response(data, status=status.HTTP_400_BAD_REQUEST)
sub = get_object_or_400(self.model, pk=sub_id)
# Verify we have permission to attach.
if not request.user.can_access(self.parent_model, 'attach', parent, sub, self.relationship, data, skip_sub_obj_read_check=created):
if not request.user.can_access(self.parent_model, 'attach', parent, sub,
self.relationship, data,
skip_sub_obj_read_check=created):
raise PermissionDenied()
# Attach the object to the collection.
@ -186,7 +218,10 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
relationship.add(sub)
if created:
return Response(data, status=status.HTTP_201_CREATED)
headers = {}
if location:
headers['Location'] = location
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
else:
return Response(status=status.HTTP_204_NO_CONTENT)
@ -199,14 +234,10 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
parent = self.get_parent_object()
parent_key = getattr(self, 'parent_key', None)
relationship = getattr(parent, self.relationship)
sub = get_object_or_400(self.model, pk=sub_id)
try:
sub = self.model.objects.get(pk=sub_id)
except self.model.DoesNotExist:
data = dict(msg='Object with id %s cannot be found' % sub_id)
return Response(data, status=status.HTTP_400_BAD_REQUEST)
if not request.user.can_access(self.parent_model, 'unattach', parent, sub, self.relationship):
if not request.user.can_access(self.parent_model, 'unattach', parent,
sub, self.relationship):
raise PermissionDenied()
if parent_key:
@ -224,19 +255,29 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
else:
return self.attach(request, *args, **kwargs)
class RetrieveAPIView(generics.RetrieveAPIView):
class RetrieveAPIView(generics.RetrieveAPIView, GenericAPIView):
pass
class RetrieveUpdateDestroyAPIView(RetrieveAPIView, generics.RetrieveUpdateDestroyAPIView):
class RetrieveUpdateAPIView(RetrieveAPIView, generics.RetrieveUpdateAPIView):
def pre_save(self, obj):
super(RetrieveUpdateDestroyAPIView, self).pre_save(obj)
super(RetrieveUpdateAPIView, self).pre_save(obj)
if isinstance(obj, PrimordialModel):
obj.created_by = self.request.user
def update(self, request, *args, **kwargs):
self.update_filter(request, *args, **kwargs)
return super(RetrieveUpdateAPIView, self).update(request, *args, **kwargs)
def update_filter(self, request, *args, **kwargs):
''' scrub any fields the user cannot/should not put/patch, based on user context. This runs after read-only serialization filtering '''
pass
class RetrieveUpdateDestroyAPIView(RetrieveUpdateAPIView, generics.RetrieveUpdateDestroyAPIView):
def destroy(self, request, *args, **kwargs):
# somewhat lame that delete has to call it's own permissions check
obj = self.model.objects.get(pk=kwargs['pk'])
obj = self.get_object()
# FIXME: Why isn't the active check being caught earlier by RBAC?
if getattr(obj, 'active', True) == False:
raise Http404()
@ -247,13 +288,5 @@ class RetrieveUpdateDestroyAPIView(RetrieveAPIView, generics.RetrieveUpdateDestr
if hasattr(obj, 'mark_inactive'):
obj.mark_inactive()
else:
raise Exception("InternalError: destroy() not implemented yet for %s" % obj)
raise NotImplementedError('destroy() not implemented yet for %s' % obj)
return HttpResponse(status=204)
def update(self, request, *args, **kwargs):
self.update_filter(request, *args, **kwargs)
return super(RetrieveUpdateDestroyAPIView, self).update(request, *args, **kwargs)
def update_filter(self, request, *args, **kwargs):
''' scrub any fields the user cannot/should not put/patch, based on user context. This runs after read-only serialization filtering '''
pass

View File

@ -147,14 +147,23 @@ class Inventory(CommonModel):
verbose_name_plural = _('inventories')
unique_together = (("name", "organization"),)
organization = models.ForeignKey(Organization, null=False, related_name='inventories')
organization = models.ForeignKey(
Organization,
null=False,
related_name='inventories',
help_text=_('Organization containing this inventory.'),
)
variables = models.TextField(
blank=True,
default='',
null=True,
help_text=_('Variables in JSON or YAML format.'),
)
has_active_failures = models.BooleanField(default=False, editable=False)
has_active_failures = models.BooleanField(
default=False,
editable=False,
help_text=_('Flag indicating whether any hosts in this inventory have failed.'),
)
def get_absolute_url(self):
return reverse('main:inventory_detail', args=(self.pk,))

View File

@ -11,8 +11,11 @@ import yaml
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.core.exceptions import ObjectDoesNotExist
from django.utils.datastructures import SortedDict
from django.utils.translation import ugettext_lazy as _
# Django REST Framework
from rest_framework.compat import get_concrete_model
from rest_framework import serializers
# AWX
@ -34,7 +37,7 @@ SUMMARIZABLE_FIELDS = (
class BaseSerializer(serializers.ModelSerializer):
# add the URL and related resources
url = serializers.SerializerMethodField('get_absolute_url')
url = serializers.SerializerMethodField('get_url')
related = serializers.SerializerMethodField('get_related')
summary_fields = serializers.SerializerMethodField('get_summary_fields')
@ -42,14 +45,34 @@ class BaseSerializer(serializers.ModelSerializer):
created = serializers.SerializerMethodField('get_created')
active = serializers.SerializerMethodField('get_active')
def get_absolute_url(self, obj):
def get_fields(self):
opts = get_concrete_model(self.opts.model)._meta
ret = super(BaseSerializer, self).get_fields()
for key, field in ret.items():
if key == 'id' and not getattr(field, 'help_text', None):
field.help_text = 'Database ID for this %s.' % unicode(opts.verbose_name)
elif key == 'url':
field.help_text = 'URL for this %s.' % unicode(opts.verbose_name)
field.type_label = 'string'
elif key == 'related':
field.help_text = 'Data structure with URLs of related resources.'
field.type_label = 'object'
elif key == 'summary_fields':
field.help_text = 'Data structure with name/description for related resources.'
field.type_label = 'object'
elif key == 'created':
field.help_text = 'Timestamp when this %s was created.' % unicode(opts.verbose_name)
field.type_label = 'datetime'
return ret
def get_url(self, obj):
if isinstance(obj, User):
return reverse('main:user_detail', args=(obj.pk,))
else:
return obj.get_absolute_url()
def get_related(self, obj):
res = dict()
res = SortedDict()
if getattr(obj, 'created_by', None):
res['created_by'] = reverse('main:user_detail', args=(obj.created_by.pk,))
return res
@ -57,12 +80,12 @@ class BaseSerializer(serializers.ModelSerializer):
def get_summary_fields(self, obj):
# return the names (at least) for various fields, so we don't have to write this
# method for each object.
summary_fields = {}
summary_fields = SortedDict()
for fk in SUMMARIZABLE_FKS:
try:
fkval = getattr(obj, fk, None)
if fkval is not None:
summary_fields[fk] = {}
summary_fields[fk] = SortedDict()
for field in SUMMARIZABLE_FIELDS:
fval = getattr(fkval, field, None)
if fval is not None:
@ -86,13 +109,13 @@ class BaseSerializer(serializers.ModelSerializer):
class UserSerializer(BaseSerializer):
password = serializers.WritableField(required=False, default='')
password = serializers.WritableField(required=False, default='',
help_text='Write-only field used to change the password.')
class Meta:
model = User
fields = ('id', 'url', 'related', 'created', 'username', 'first_name',
'last_name', 'email', 'is_active', 'is_superuser',
'password')
'last_name', 'email', 'is_superuser', 'password')
def to_native(self, obj):
ret = super(UserSerializer, self).to_native(obj)
@ -152,7 +175,7 @@ class OrganizationSerializer(BaseSerializer):
class ProjectSerializer(BaseSerializer):
playbooks = serializers.Field(source='playbooks')
playbooks = serializers.Field(source='playbooks', help_text='Array ')
class Meta:
model = Project
@ -163,7 +186,7 @@ class ProjectSerializer(BaseSerializer):
res.update(dict(
organizations = reverse('main:project_organizations_list', args=(obj.pk,)),
teams = reverse('main:project_teams_list', args=(obj.pk,)),
playbooks = reverse('main:project_detail_playbooks', args=(obj.pk,)),
playbooks = reverse('main:project_playbooks', args=(obj.pk,)),
))
return res
@ -190,7 +213,7 @@ class BaseSerializerWithVariables(BaseSerializer):
def validate_variables(self, attrs, source):
try:
json.loads(attrs[source].strip() or '{}')
json.loads(attrs.get(source, '').strip() or '{}')
except ValueError:
try:
yaml.safe_load(attrs[source])
@ -211,8 +234,9 @@ class InventorySerializer(BaseSerializerWithVariables):
hosts = reverse('main:inventory_hosts_list', args=(obj.pk,)),
groups = reverse('main:inventory_groups_list', args=(obj.pk,)),
root_groups = reverse('main:inventory_root_groups_list', args=(obj.pk,)),
variable_data = reverse('main:inventory_variable_detail', args=(obj.pk,)),
organization = reverse('main:organization_detail', args=(obj.organization.pk,)),
variable_data = reverse('main:inventory_variable_data', args=(obj.pk,)),
script = reverse('main:inventory_script_view', args=(obj.pk,)),
organization = reverse('main:organization_detail', args=(obj.organization.pk,)),
))
return res
@ -225,7 +249,7 @@ class HostSerializer(BaseSerializerWithVariables):
def get_related(self, obj):
res = super(HostSerializer, self).get_related(obj)
res.update(dict(
variable_data = reverse('main:host_variable_detail', args=(obj.pk,)),
variable_data = reverse('main:host_variable_data', args=(obj.pk,)),
inventory = reverse('main:inventory_detail', args=(obj.inventory.pk,)),
groups = reverse('main:host_groups_list', args=(obj.pk,)),
all_groups = reverse('main:host_all_groups_list', args=(obj.pk,)),
@ -247,7 +271,7 @@ class GroupSerializer(BaseSerializerWithVariables):
def get_related(self, obj):
res = super(GroupSerializer, self).get_related(obj)
res.update(dict(
variable_data = reverse('main:group_variable_detail', args=(obj.pk,)),
variable_data = reverse('main:group_variable_data', args=(obj.pk,)),
hosts = reverse('main:group_hosts_list', args=(obj.pk,)),
children = reverse('main:group_children_list', args=(obj.pk,)),
all_hosts = reverse('main:group_all_hosts_list', args=(obj.pk,)),

View File

@ -0,0 +1,57 @@
The resulting data structure contains:
{
"count": 99,
"next": null,
"previous": null,
"results": [
...
]
}
The `count` field indicates the total number of {{ model_verbose_name_plural }}
found for the given query. The `next` and `previous` fields provides links to
additional results if there are more than will fit on a single page. The
`results` list contains zero or more {{ model_verbose_name }} records.
## Results
Each {{ model_verbose_name }} data structure includes the following fields:
{% include "main/_result_fields_common.md" %}
## Sorting
To specify that {{ model_verbose_name_plural }} are returned in a particular
order, use the `order_by` query string parameter on the GET request.
?order_by={{ order_field }}
Prefix the field name with a dash `-` to sort in reverse:
?order_by=-{{ order_field }}
## Pagination
Use the `page_size` query string parameter to change the number of results
returned for each request. Use the `page` query string parameter to retrieve
a particular page of results.
?page_size=100&page=2
The `previous` and `next` links returned with the results will set these query
string parameters automatically.
## Filtering
Any additional query string parameters may be used to filter the list of
results returned to those matching a given value. Only fields that exist in
the database can be used for filtering.
?{{ order_field }}=value
Field lookups may also be used for slightly more advanced queries, for example:
?{{ order_field }}__startswith=A
?{{ order_field }}__endsswith=C
?{{ order_field }}__contains=ABC

View File

@ -0,0 +1,6 @@
{% for fn, fm in serializer_fields.items %}{% spaceless %}
{% if not write_only or not fm.read_only %}
* `{{ fn }}`: {{ fm.help_text|capfirst }} ({{ fm.type }}{% if fm.required %}, required{% endif %}{% if fm.read_only %}, read-only{% endif %})
{% endif %}
{% endspaceless %}
{% endfor %}

View File

@ -0,0 +1,4 @@
The root of the AWX REST API.
Make a GET request to this resource to obtain information about the available
API versions.

View File

@ -0,0 +1,12 @@
Site configuration settings and general information.
Make a GET request to this resource to retrieve the configuration containing
the following fields (some fields may not be visible to all users):
* `project_base_dir`: Path on the server where projects and playbooks are \
stored.
* `project_local_paths`: List of directories beneath `project_base_dir` to
use when creating/editing a project.
* `time_zone`: The configured time zone for the server.
* `license_info`: Information about the current license.
* `version`: Version of AWX package installed.

View File

@ -0,0 +1,4 @@
Version 1 of the AWX REST API.
Make a GET request to this resource to obtain a list of all child resources
available via the API.

View File

@ -0,0 +1 @@
{{ docstring }}

View File

@ -0,0 +1,23 @@
Make a POST request to this resource with `username` and `password` fields to
obtain an authentication token to use for subsequent requests.
Example JSON to POST (content type is `application/json`):
{"username": "user", "password": "my pass"}
Example form data to post (content type is `application/x-www-form-urlencoded`):
username=user&password=my%20pass
If the username and password provided are valid, the response will contain a
`token` field with the authentication token to use:
{"token": "8f17825cf08a7efea124f2638f3896f6637f8745"}
Otherwise, the response will indicate the error that occurred and return a 4xx
status code.
For subsequent requests, pass the token via the HTTP `Authenticate` request
header:
Authenticate: Token 8f17825cf08a7efea124f2638f3896f6637f8745

View File

@ -0,0 +1,9 @@
# Retrieve {{ model_verbose_name|title }} Variable Data:
Make a GET request to this resource to retrieve all variables defined for this
{{ model_verbose_name }}.
# Update {{ model_verbose_name|title }} Variable Data:
Make a PUT request to this resource to update variables defined for this
{{ model_verbose_name }}.

View File

@ -0,0 +1,7 @@
# List All {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
Make a GET request to this resource to retrieve a list of all
{{ model_verbose_name_plural }} directly or indirectly belonging to this
{{ parent_model_verbose_name }}.
{% include "main/_list_common.md" %}

View File

@ -0,0 +1,7 @@
# List All {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
Make a GET request to this resource to retrieve a list of all
{{ model_verbose_name_plural }} of which the selected
{{ parent_model_verbose_name }} is directly or indirectly a member.
{% include "main/_list_common.md" %}

View File

@ -0,0 +1,7 @@
# List Root {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
Make a GET request to this resource to retrieve a list of root (top-level)
{{ model_verbose_name_plural }} associated with this
{{ parent_model_verbose_name }}.
{% include "main/_list_common.md" %}

View File

@ -0,0 +1,11 @@
Generate inventory group and host data as needed for an inventory script.
Make a GET request to this resource without query parameters to retrieve a JSON
object containing groups, including the hosts, children and variables for each
group. The response data is equivalent to that returned by passing the
`--list` argument to an inventory script.
Make a GET request to this resource with a query string similar to
`?host=HOSTNAME` to retrieve a JSON object containing host variables for the
specified host. The response data is equivalent to that returned by passing
the `--host HOSTNAME` argument to an inventory script.

View File

@ -0,0 +1,10 @@
# Cancel Job
Make a GET request to this resource to determine if the job can be cancelled.
The response will include the following field:
* `can_cancel`: Indicates whether this job can be canceled (boolean, read-only)
Make a POST request to this resource to cancel a pending or running job. The
response status code will be 202 if successful, or 405 if the job cannot be
canceled.

View File

@ -0,0 +1,5 @@
{% include "main/list_create_api_view.md" %}
If the `job_template` field is specified, any fields not explicitly provided
for the new job (except `name` and `description`) will use the default values
from the job template.

View File

@ -0,0 +1,15 @@
# Start Job
Make a GET request to this resource to determine if the job can be started and
whether any passwords are required to start the job. The response will include
the following fields:
* `can_start`: Flag indicating if this job can be started (boolean, read-only)
* `passwords_needed_to_start`: Password names required to start the job (array, read-only)
Make a POST request to this resource to start the job. If any passwords are
required, they must be passed via POST data.
If successful, the response status code will be 202. If any required passwords
are not provided, a 400 status code will be returned. If the job cannot be
started, a 405 status code will be returned.

View File

@ -0,0 +1,33 @@
The job template callback allows for empheral hosts to launch a new job.
Configure a host to POST to this resource, passing the `host_config_key`
parameter, to start a new job limited to only the requesting host. In the
examples below, replace the `N` parameter with the `id` of the job template
and the `HOST_CONFIG_KEY` with the `host_config_key` associated with the
job template.
For example, using curl:
curl --data-urlencode host_config_key=HOST_CONFIG_KEY http://server/api/v1/job_templates/N/callback/
Or using wget:
wget -O /dev/null --post-data="host_config_key=HOST_CONFIG_KEY" http://server/api/v1/job_templates/N/callback/
The response will return status 202 if the request is valid, 403 for an
invalid host config key, or 400 if the host cannot be determined from the
address making the request.
A GET request may be used to verify that the correct host will be selected.
This request must authenticate as a valid user with permission to edit the
job template. For example:
curl http://user:password@server/api/v1/job_templates/N/callback/
The response will include the host config key as well as the host name(s)
that would match the request:
{
"host_config_key": "HOST_CONFIG_KEY",
"matching_hosts": ["hostname"]
}

View File

@ -0,0 +1,6 @@
{% extends "main/sub_list_create_api_view.md" %}
{% block post_create %}
Any fields not explicitly provided for the new job (except `name` and
`description`) will use the default values from the job template.
{% endblock %}

View File

@ -0,0 +1,6 @@
# List {{ model_verbose_name_plural|title }}:
Make a GET request to this resource to retrieve the list of
{{ model_verbose_name_plural }}.
{% include "main/_list_common.md" %}

View File

@ -0,0 +1,10 @@
{% include "main/list_api_view.md" %}
# Create {{ model_verbose_name_plural|title }}:
Make a POST request to this resource with the following {{ model_verbose_name }}
fields to create a new {{ model_verbose_name }}:
{% with write_only=1 %}
{% include "main/_result_fields_common.md" %}
{% endwith %}

View File

@ -0,0 +1,3 @@
{% with model_verbose_name="admin user" model_verbose_name_plural="admin users" %}
{% include "main/sub_list_create_api_view.md" %}
{% endwith %}

View File

@ -0,0 +1,4 @@
# Retrieve {{ model_verbose_name|title }} Playbooks:
Make GET request to this resource to retrieve a list of playbooks available
for this {{ model_verbose_name }}.

View File

@ -0,0 +1,6 @@
# Retrieve {{ model_verbose_name|title }}:
Make GET request to this resource to retrieve a single {{ model_verbose_name }}
record containing the following fields:
{% include "main/_result_fields_common.md" %}

View File

@ -0,0 +1,18 @@
{% include "main/retrieve_api_view.md" %}
# Update {{ model_verbose_name|title }}:
Make a PUT or PATCH request to this resource to update this
{{ model_verbose_name }}. The following fields may be modified:
{% with write_only=1 %}
{% include "main/_result_fields_common.md" %}
{% endwith %}
For a PUT request, include **all** fields in the request.
For a PATCH request, include only the fields that are being modified.
# Delete {{ model_verbose_name|title }}:
Make a DELETE request to this resource to delete this {{ model_verbose_name }}.

View File

@ -0,0 +1,7 @@
# List {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
Make a GET request to this resource to retrieve a list of
{{ model_verbose_name_plural }} associated with the selected
{{ parent_model_verbose_name }}.
{% include "main/_list_common.md" %}

View File

@ -0,0 +1,37 @@
{% include "main/sub_list_api_view.md" %}
# Create {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
Make a POST request to this resource with the following {{ model_verbose_name }}
fields to create a new {{ model_verbose_name }} associated with this
{{ parent_model_verbose_name }}.
{% with write_only=1 %}
{% include "main/_result_fields_common.md" %}
{% endwith %}
{% block post_create %}{% endblock %}
{% if parent_key %}
# Remove {{ parent_model_verbose_name|title }} {{ model_verbose_name_plural|title }}:
Make a POST request to this resource with `id` and `disassociate` fields to
delete the associated {{ model_verbose_name }}.
{
"id": 123,
"disassociate": true
}
{% else %}
# Add {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
Make a POST request to this resource with only an `id` field to associate an
existing {{ model_verbose_name }} with this {{ parent_model_verbose_name }}.
# Remove {{ model_verbose_name_plural|title }} from this {{ parent_model_verbose_name|title }}:
Make a POST request to this resource with `id` and `disassociate` fields to
remove the {{ model_verbose_name }} from this {{ parent_model_verbose_name }}
without deleting the {{ model_verbose_name }}.
{% endif %}

View File

@ -0,0 +1,7 @@
# List {{ model_verbose_name_plural|title }} Administered by this {{ parent_model_verbose_name|title }}:
Make a GET request to this resource to retrieve a list of
{{ model_verbose_name_plural }} of which the selected
{{ parent_model_verbose_name }} is an admin.
{% include "main/_list_common.md" %}

View File

@ -0,0 +1,7 @@
Make a GET request to retrieve user information about the current user.
One result should be returned containing the following fields:
{% include "main/_result_fields_common.md" %}
Use the primary URL for the user (/api/v1/users/N/) to modify the user.

View File

@ -342,7 +342,7 @@ class InventoryTest(BaseTest):
vars_c = dict(asdf=5555, dog='mouse', cat='mogwai', unstructured=dict(a=[3,0,3],b=dict(z=2600)))
# attempting to get a variable object creates it, even though it does not already exist
vdata_url = reverse('main:host_variable_detail', args=(added_by_collection_a['id'],))
vdata_url = reverse('main:host_variable_data', args=(added_by_collection_a['id'],))
got = self.get(vdata_url, expect=200, auth=self.get_super_credentials())
self.assertEquals(got, {})
@ -373,8 +373,8 @@ class InventoryTest(BaseTest):
vars_c = dict(asdf=9999, dog='pluto', cat='five', unstructured=dict(a=[3,3,3],b=dict(z=5)))
groups = Group.objects.all()
vdata1_url = reverse('main:group_variable_detail', args=(groups[0].pk,))
vdata2_url = reverse('main:group_variable_detail', args=(groups[1].pk,))
vdata1_url = reverse('main:group_variable_data', args=(groups[0].pk,))
vdata2_url = reverse('main:group_variable_data', args=(groups[1].pk,))
# a super user can associate variable objects with groups
got = self.get(vdata1_url, expect=200, auth=self.get_super_credentials())
@ -399,7 +399,7 @@ class InventoryTest(BaseTest):
vars_b = dict(asdf=2736, dog='benji', cat='garfield', unstructured=dict(a=[2,2,2],b=dict(x=3,y=4)))
vars_c = dict(asdf=7692, dog='buck', cat='sylvester', unstructured=dict(a=[3,3,3],b=dict(z=5)))
vdata_url = reverse('main:inventory_variable_detail', args=(self.inventory_a.pk,))
vdata_url = reverse('main:inventory_variable_data', args=(self.inventory_a.pk,))
# a super user can associate variable objects with inventory
got = self.get(vdata_url, expect=200, auth=self.get_super_credentials())

View File

@ -235,7 +235,7 @@ class ProjectsTest(BaseTest):
self.get(project, expect=404, auth=self.get_normal_credentials())
# can list playbooks for projects
proj_playbooks = reverse('main:project_detail_playbooks', args=(self.projects[2].pk,))
proj_playbooks = reverse('main:project_playbooks', args=(self.projects[2].pk,))
got = self.get(proj_playbooks, expect=200, auth=self.get_super_credentials())
self.assertEqual(got, self.projects[2].playbooks)

View File

@ -33,7 +33,7 @@ user_urls = patterns('awx.main.views',
project_urls = patterns('awx.main.views',
url(r'^$', 'project_list'),
url(r'^(?P<pk>[0-9]+)/$', 'project_detail'),
url(r'^(?P<pk>[0-9]+)/playbooks/$', 'project_detail_playbooks'),
url(r'^(?P<pk>[0-9]+)/playbooks/$', 'project_playbooks'),
url(r'^(?P<pk>[0-9]+)/organizations/$', 'project_organizations_list'),
url(r'^(?P<pk>[0-9]+)/teams/$', 'project_teams_list'),
)
@ -53,14 +53,14 @@ inventory_urls = patterns('awx.main.views',
url(r'^(?P<pk>[0-9]+)/hosts/$', 'inventory_hosts_list'),
url(r'^(?P<pk>[0-9]+)/groups/$', 'inventory_groups_list'),
url(r'^(?P<pk>[0-9]+)/root_groups/$', 'inventory_root_groups_list'),
url(r'^(?P<pk>[0-9]+)/variable_data/$', 'inventory_variable_detail'),
url(r'^(?P<pk>[0-9]+)/variable_data/$', 'inventory_variable_data'),
url(r'^(?P<pk>[0-9]+)/script/$', 'inventory_script_view'),
)
host_urls = patterns('awx.main.views',
url(r'^$', 'host_list'),
url(r'^(?P<pk>[0-9]+)/$', 'host_detail'),
url(r'^(?P<pk>[0-9]+)/variable_data/$', 'host_variable_detail'),
url(r'^(?P<pk>[0-9]+)/variable_data/$', 'host_variable_data'),
url(r'^(?P<pk>[0-9]+)/groups/$', 'host_groups_list'),
url(r'^(?P<pk>[0-9]+)/all_groups/$', 'host_all_groups_list'),
url(r'^(?P<pk>[0-9]+)/job_events/', 'host_job_events_list'),
@ -73,7 +73,7 @@ group_urls = patterns('awx.main.views',
url(r'^(?P<pk>[0-9]+)/children/$', 'group_children_list'),
url(r'^(?P<pk>[0-9]+)/hosts/$', 'group_hosts_list'),
url(r'^(?P<pk>[0-9]+)/all_hosts/$', 'group_all_hosts_list'),
url(r'^(?P<pk>[0-9]+)/variable_data/$', 'group_variable_detail'),
url(r'^(?P<pk>[0-9]+)/variable_data/$', 'group_variable_data'),
url(r'^(?P<pk>[0-9]+)/job_events/$', 'group_job_events_list'),
url(r'^(?P<pk>[0-9]+)/job_host_summaries/$', 'group_job_host_summaries_list'),
)

View File

@ -6,7 +6,8 @@ import sys
# Django REST Framework
from rest_framework.exceptions import ParseError, PermissionDenied
__all__ = ['get_object_or_400', 'get_object_or_403', 'camelcase_to_underscore']
__all__ = ['get_object_or_400', 'get_object_or_403', 'camelcase_to_underscore',
'get_awx_version']
def get_object_or_400(klass, *args, **kwargs):
'''
@ -51,3 +52,14 @@ class RequireDebugTrueOrTest(logging.Filter):
def filter(self, record):
from django.conf import settings
return settings.DEBUG or 'test' in sys.argv
def get_awx_version():
'''
Return AWX version as reported by setuptools.
'''
from awx import __version__
try:
import pkg_resources
return pkg_resources.require('awx')[0].version
except:
return __version__

View File

@ -13,17 +13,17 @@ from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404, render_to_response
from django.template import RequestContext
from django.utils.datastructures import SortedDict
# Django REST Framework
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.exceptions import PermissionDenied
from rest_framework import generics
from rest_framework.parsers import YAMLParser
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.renderers import YAMLRenderer
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.views import APIView
from rest_framework import status
# AWX
from awx.main.authentication import JobTaskAuthentication
@ -54,10 +54,6 @@ def handle_500(request):
return handle_error(request, 500)
class ApiRootView(APIView):
'''
This resource is the root of the AWX REST API and provides
information about the available API versions.
'''
permission_classes = (AllowAny,)
view_name = 'REST API'
@ -76,11 +72,6 @@ class ApiRootView(APIView):
return Response(data)
class ApiV1RootView(APIView):
'''
Version 1 of the REST API.
Subject to change until the final 1.2 release.
'''
permission_classes = (AllowAny,)
view_name = 'Version 1'
@ -106,17 +97,6 @@ class ApiV1RootView(APIView):
return Response(data)
class ApiV1ConfigView(APIView):
'''
Various sitewide configuration settings (some may only be visible to
superusers or organization admins):
* `project_base_dir`: Path on the server where projects and playbooks are \
stored.
* `project_local_paths`: List of directories beneath `project_base_dir` to
use when creating/editing a project.
* `time_zone`: The configured time zone for the server.
* `license_info`: Information about the current license.
'''
permission_classes = (IsAuthenticated,)
view_name = 'Configuration'
@ -130,6 +110,7 @@ class ApiV1ConfigView(APIView):
data = dict(
time_zone=settings.TIME_ZONE,
license_info=license_data,
version=get_awx_version(),
)
if request.user.is_superuser or request.user.admin_of_organizations.filter(active=True).count():
data.update(dict(
@ -139,29 +120,7 @@ class ApiV1ConfigView(APIView):
return Response(data)
class AuthTokenView(ObtainAuthToken):
'''
POST username and password to this resource to obtain an authentication
token for subsequent requests.
Example JSON to post (application/json):
{"username": "user", "password": "my pass"}
Example form data to post (application/x-www-form-urlencoded):
username=user&password=my%20pass
If the username and password are valid, the response should be:
{"token": "8f17825cf08a7efea124f2638f3896f6637f8745"}
Otherwise, the response will indicate the error that occurred.
For subsequent requests, pass the token via the HTTP request headers:
Authenticate: Token 8f17825cf08a7efea124f2638f3896f6637f8745
'''
class AuthTokenView(ObtainAuthToken, APIView):
permission_classes = (AllowAny,)
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
@ -273,7 +232,7 @@ class ProjectDetail(RetrieveUpdateDestroyAPIView):
model = Project
serializer_class = ProjectSerializer
class ProjectDetailPlaybooks(RetrieveAPIView):
class ProjectPlaybooks(RetrieveAPIView):
model = Project
serializer_class = ProjectPlaybooksSerializer
@ -301,8 +260,7 @@ class UserMeList(ListAPIView):
model = User
serializer_class = UserSerializer
view_name = 'Me!'
view_name = 'Me'
def get_queryset(self):
return self.model.objects.filter(pk=self.request.user.pk)
@ -522,37 +480,28 @@ class InventoryRootGroupsList(SubListCreateAPIView):
sublist_qs = parent.groups.exclude(parents__pk__in=all_pks).distinct()
return qs & sublist_qs
class BaseVariableDetail(RetrieveUpdateDestroyAPIView):
class BaseVariableData(RetrieveUpdateAPIView):
parser_classes = api_settings.DEFAULT_PARSER_CLASSES + [YAMLParser]
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [YAMLRenderer]
is_variable_data = True # Special flag for permissions check.
class InventoryVariableDetail(BaseVariableDetail):
class InventoryVariableData(BaseVariableData):
model = Inventory
serializer_class = InventoryVariableDataSerializer
class HostVariableDetail(BaseVariableDetail):
class HostVariableData(BaseVariableData):
model = Host
serializer_class = HostVariableDataSerializer
class GroupVariableDetail(BaseVariableDetail):
class GroupVariableData(BaseVariableData):
model = Group
serializer_class = GroupVariableDataSerializer
class InventoryScriptView(RetrieveAPIView):
'''
Return inventory group and host data as needed for an inventory script.
Without query parameters, return groups with hosts, children and vars
(equivalent to the --list parameter to an inventory script).
With ?host=HOSTNAME, return host vars for the given host (equivalent to the
--host HOSTNAME parameter to an inventory script).
'''
model = Inventory
authentication_classes = [JobTaskAuthentication] + \
@ -568,48 +517,30 @@ class InventoryScriptView(RetrieveAPIView):
name=hostname)
data = host.variables_dict
else:
data = {}
data = SortedDict()
if self.object.variables_dict:
data['all'] = SortedDict()
data['all']['vars'] = self.object.variables_dict
for group in self.object.groups.filter(active=True):
hosts = group.hosts.filter(active=True)
children = group.children.filter(active=True)
group_info = {
'hosts': list(hosts.values_list('name', flat=True)),
'children': list(children.values_list('name', flat=True)),
'vars': group.variables_dict,
}
group_info = SortedDict()
group_info['hosts'] = list(hosts.values_list('name', flat=True))
group_info['children'] = list(children.values_list('name', flat=True))
group_info['vars'] = group.variables_dict
data[group.name] = group_info
# this confuses the inventory script if the group
# has children set and no variables or hosts.
# no other reason to do this right?
#
# group_info = dict(filter(lambda x: bool(x[1]),
# group_info.items()))
if group_info.keys() in ([], ['hosts']):
data[group.name] = group_info.get('hosts', [])
else:
data[group.name] = group_info
if self.object.variables_dict:
data['all'] = {
'vars': self.object.variables_dict,
}
# workaround for Ansible inventory bug (github #3687), localhost must
# be explicitly listed in the all group for dynamic inventory
# scripts to pick it up
localhost = Host.objects.filter(inventory=self.object, name='localhost').count()
localhost2 = Host.objects.filter(inventory=self.object, name='127.0.0.1').count()
if localhost or localhost2:
if not 'all' in data:
data['all'] = {}
data['all']['hosts'] = []
if localhost:
data['all']['hosts'].append('localhost')
if localhost2:
data['all']['hosts'].append('127.0.0.1')
# workaround for Ansible inventory bug (github #3687), localhost
# must be explicitly listed in the all group for dynamic inventory
# scripts to pick it up.
localhost_names = ('localhost', '127.0.0.1', '::1')
localhosts_qs = self.object.hosts.filter(active=True,
name__in=localhost_names)
localhosts = list(localhosts_qs.values_list('name', flat=True))
if localhosts:
data.setdefault('all', SortedDict())
data['all']['hosts'] = localhosts
return Response(data)
@ -618,51 +549,12 @@ class JobTemplateList(ListCreateAPIView):
model = JobTemplate
serializer_class = JobTemplateSerializer
def _get_queryset(self):
return self.request.user.get_queryset(self.model)
class JobTemplateDetail(RetrieveUpdateDestroyAPIView):
model = JobTemplate
serializer_class = JobTemplateSerializer
class JobTemplateCallback(generics.GenericAPIView):
'''
The job template callback allows for empheral hosts to launch a new job.
Configure a host to POST to this resource, passing the `host_config_key`
parameter, to start a new job limited to only the requesting host. In the
examples below, replace the `N` parameter with the `id` of the job template
and the `HOST_CONFIG_KEY` with the `host_config_key` associated with the
job template.
For example, using curl:
curl --data-urlencode host_config_key=HOST_CONFIG_KEY http://server/api/v1/job_templates/N/callback/
Or using wget:
wget -O /dev/null --post-data="host_config_key=HOST_CONFIG_KEY" http://server/api/v1/job_templates/N/callback/
The response will return status 202 if the request is valid, 403 for an
invalid host config key, or 400 if the host cannot be determined from the
address making the request.
A GET request may be used to verify that the correct host will be selected.
This request must authenticate as a valid user with permission to edit the
job template. For example:
curl http://user:password@server/api/v1/job_templates/N/callback/
The response will include the host config key as well as the host name(s)
that would match the request:
{
"host_config_key": "HOST_CONFIG_KEY",
"matching_hosts": ["hostname"]
}
'''
class JobTemplateCallback(GenericAPIView):
model = JobTemplate
permission_classes = (JobTemplateCallbackPermission,)
@ -782,9 +674,6 @@ class JobList(ListCreateAPIView):
model = Job
serializer_class = JobSerializer
def _get_queryset(self):
return self.model.objects.all() # FIXME
class JobDetail(RetrieveUpdateDestroyAPIView):
model = Job
@ -797,7 +686,7 @@ class JobDetail(RetrieveUpdateDestroyAPIView):
return self.http_method_not_allowed(request, *args, **kwargs)
return super(JobDetail, self).update(request, *args, **kwargs)
class JobStart(generics.GenericAPIView):
class JobStart(GenericAPIView):
model = Job
is_job_start = True
@ -823,7 +712,7 @@ class JobStart(generics.GenericAPIView):
else:
return self.http_method_not_allowed(request, *args, **kwargs)
class JobCancel(generics.GenericAPIView):
class JobCancel(GenericAPIView):
model = Job
is_job_cancel = True
@ -849,8 +738,7 @@ class BaseJobHostSummariesList(SubListAPIView):
serializer_class = JobHostSummarySerializer
parent_model = None # Subclasses must define this attribute.
relationship = 'job_host_summaries'
view_name = 'Job Host Summary List'
view_name = 'Job Host Summaries List'
class HostJobHostSummariesList(BaseJobHostSummariesList):
@ -885,7 +773,6 @@ class JobEventChildrenList(SubListAPIView):
serializer_class = JobEventSerializer
parent_model = JobEvent
relationship = 'children'
view_name = 'Job Event Children List'
class JobEventHostsList(SubListAPIView):
@ -894,7 +781,6 @@ class JobEventHostsList(SubListAPIView):
serializer_class = HostSerializer
parent_model = JobEvent
relationship = 'hosts'
view_name = 'Job Event Hosts List'
class BaseJobEventsList(SubListAPIView):
@ -903,6 +789,7 @@ class BaseJobEventsList(SubListAPIView):
serializer_class = JobEventSerializer
parent_model = None # Subclasses must define this attribute.
relationship = 'job_events'
view_name = 'Job Events List'
class HostJobEventsList(BaseJobEventsList):

View File

@ -12,18 +12,18 @@ html body {
}
html body .navbar .navbar-inner {
border-top: none;
height: 20px;
height: 54px;
}
html body .navbar-inverse .navbar-inner {
background-color: #36454F;
background-image: -moz-linear-gradient(top, #36454F, #36454F);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#36454F), to(#36454F));
background-image: -webkit-linear-gradient(top, #36454F, #36454F);
background-image: -o-linear-gradient(top, #36454F, #36454F);
background-image: linear-gradient(to bottom, #36454F, #36454F);
background-color: #171717;
background-image: -moz-linear-gradient(top, #171717, #171717);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#171717), to(#171717));
background-image: -webkit-linear-gradient(top, #171717, #171717);
background-image: -o-linear-gradient(top, #171717, #171717);
background-image: linear-gradient(to bottom, #171717, #171717);
background-repeat: repeat-x;
border-color: #36454F;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#36454F', endColorstr='#36454F', GradientType=0);
border-color: #171717;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#171717', endColorstr='#171717', GradientType=0);
}
html body .navbar-inverse .nav > li > a {
color: #A9A9A9;
@ -33,7 +33,7 @@ html body .navbar-inverse .nav > li > a:focus {
color: #2078be;
}
html body .navbar .brand img {
width: 130px;
width: 200px;
margin-top: -6px;
margin-right: 0.5em;
}
@ -45,6 +45,15 @@ html body .navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle {
html body .navbar-inverse .brand {
font-size: 1.2em;
color: #fff;
padding-top: 6px;
padding-left: 4px;
}
html body .navbar-inverse .nav.pull-right {
margin-top: 8px;
margin-right: -8px;
}
html body ul.breadcrumb {
margin-top: 72px;
}
span.powered-by .version {
color: #ddd;
@ -88,20 +97,35 @@ html body .description {
padding-bottom: 0;
display: none;
}
.footer {
margin-top: 0.5em;
#footer {
font-size: 0.8em;
text-align: center;
padding-bottom: 1em;
}
.footer a,
.footer a:hover {
#footer a,
#footer a:hover {
color: #333;
}
img.awxlogo {
width: 125px;
margin-bottom: 0.5em;
}
html body .wrapper {
min-height: 1%;
height: auto !important;
margin: 0 auto 0;
}
html body #footer {
height: auto !important;
}
html body #push {
height: 0;
}
</style>
{% endblock %}
{% block branding %}
<a class="brand" href="/api/"><img class="logo" src="{{ STATIC_URL }}img/ansibleworks-logo.png">{% trans 'REST API' %}</a>
<a class="brand" href="/api/"><img class="logo" src="{{ STATIC_URL }}img/logo.png">{% trans 'REST API' %}</a>
{% endblock %}
{% block userlinks %}
@ -113,7 +137,9 @@ html body .description {
{% endblock %}
{% block footer %}
<div class="footer">Copyright &copy; 2013 <a href="http://www.ansibleworks.com/">AnsibleWorks, Inc.</a> All rights reserved.<br />
<div id="footer">
<img class="awxlogo" src="{{ STATIC_URL }}img/AWX_logo.png" /><br/>
Copyright &copy; 2013 <a href="http://www.ansibleworks.com/">AnsibleWorks, Inc.</a> All rights reserved.<br />
1482 East Valley Road, Suite 888 &middot; Montecito, California 9308 &middot; <a href="tel:18008250212">+1-800-825-0212<a/></div>
{% endblock %}
@ -121,13 +147,51 @@ html body .description {
{{ block.super }}
<script type="text/javascript">
$(function() {
// Make linkes from relative URLs to resources.
// Make links from relative URLs to resources.
$('span.str').each(function() {
// Remove REST API links within data.
if ($(this).parent('a').size()) {
$(this).unwrap();
}
var s = $(this).html();
if (s.match(/^\"\/.+\/\"$/) || s.match(/^\"\/.+\/\?.*\"$/)) {
$(this).html('"<a href=' + s + '>' + s.replace(/\"/g, '') + '</a>"');
}
});
// Make links for all inventory script hosts.
$('.request-info .pln:contains("script")').each(function() {
$('.response-info span.str:contains("hosts")').each(function() {
if ($(this).text() != '"hosts"') {
return;
}
var hosts_state = 0;
$(this).find('~ span').each(function() {
if (hosts_state == 0) {
if ($(this).is('span.pun') && $(this).text() == '[') {
hosts_state = 1;
}
}
else if (hosts_state == 1) {
if ($(this).is('span.pun') && ($(this).text() == ']' || $(this).text() == '],')) {
hosts_state = 2;
}
else if ($(this).is('span.str')) {
if ($(this).text() == '"') {
}
else if ($(this).text().match(/^\".+\"$/)) {
var s = $(this).text().replace(/\"/g, '');
$(this).html('"<a href="' + '?host=' + s + '">' + s + '</a>"');
}
else {
var s = $(this).text();
$(this).html('<a href="' + '?host=' + s + '">' + s + '</a>');
}
}
}
});
});
});
// Add classes/icons for dynamically showing/hiding help.
if ($('.description').html()) {
$('.description').addClass('well').addClass('well-small').addClass('prettyprint');
$('.description').prepend('<a class="hide-description pull-right" href="#" title="Hide Description"><i class="icon-remove"></i></a>');
@ -140,6 +204,9 @@ $(function() {
$('.description').slideToggle('fast');
return false;
});
if (window.location.hash == '#showhelp') {
$('.description').slideDown('fast');
}
}
$('.btn-primary').removeClass('btn-primary').addClass('btn-success');
});