+
+from distutils.version import StrictVersion
+
+import rest_framework
from rest_framework import serializers
from rest_framework import fields
-if False:
+
+if StrictVersion(rest_framework.__version__) < StrictVersion("3.0.0"):
class BooleanField(serializers.BooleanField):
"""
Version of BooleanField which handles fields which are 1,0
BooleanField = fields.BooleanField
-if False:
+if StrictVersion(rest_framework.__version__) < StrictVersion("3.0.0"):
class UuidField(serializers.CharField):
"""
For strings like Ceph service UUIDs and Ceph cluster FSIDs
# rest-framework 3 has built in uuid field.
UuidField = fields.UUIDField
-if False:
+if StrictVersion(rest_framework.__version__) < StrictVersion("3.0.0"):
class EnumField(serializers.CharField):
def __init__(self, mapping, *args, **kwargs):
super(EnumField, self).__init__(*args, **kwargs)
+++ /dev/null
-
-from django.contrib.auth.models import User
-from django.utils import dateformat
-
-from rest_framework import serializers
-import dateutil.parser
-
-
-def to_unix(t):
- if t is None:
- return None
- return int(dateformat.format(t, 'U')) * 1000
-
-
-class ClusterSerializer(serializers.Serializer):
- class Meta:
- fields = ('cluster_update_time', 'cluster_update_time_unix', 'id', 'name')
-
- cluster_update_time = serializers.SerializerMethodField('get_update_time')
- name = serializers.Field()
- id = serializers.Field()
-
- # FIXME: we should not be sending out time in two formats: if API consumers want
- # unix timestamps they can do the conversion themselves.
- cluster_update_time_unix = serializers.SerializerMethodField('get_update_time_unix')
-
- def get_update_time(self, obj):
- return obj.update_time
-
- def get_update_time_unix(self, obj):
- update_time = dateutil.parser.parse(obj.update_time)
- return to_unix(update_time)
-
- # NB calamari 1.0 had cluster_atttempt_time, which no longer makes sense
- # because we're listening for events, not polling. TODO: expunge from GUI code.
-
-
-class UserSerializer(serializers.ModelSerializer):
- """
- Serializer for the Django User model.
-
- Used to expose a django-rest-framework user management resource.
- """
- class Meta:
- model = User
- fields = ('id', 'username', 'password', 'email')
-
- def to_native(self, obj):
- # Before conversion, remove the password field. This prevents the hash
- # from being displayed when requesting user details.
- if 'password' in self.fields:
- del self.fields['password']
- return super(UserSerializer, self).to_native(obj)
-
- def restore_object(self, attrs, instance=None):
- user = super(UserSerializer, self).restore_object(attrs, instance)
- if user:
- # This will perform the Django-specific password obfuscation
- user.set_password(attrs['password'])
- return user
-
-
-class ClusterSpaceSerializer(serializers.Serializer):
- space = serializers.Field()
-
- class Meta:
- fields = ('space',)
-
-
-class ClusterHealthSerializer(serializers.Serializer):
- report = serializers.Field()
-
- class Meta:
- fields = ('report', 'cluster_update_time', 'cluster_update_time_unix')
-
- # FIXME: should not be copying this field onto health counters etc, clients should get
- # it by querying the cluster directly.
- cluster_update_time = serializers.Field()
- cluster_update_time_unix = serializers.SerializerMethodField('get_cluster_update_time_unix')
-
- def get_cluster_update_time_unix(self, obj):
- update_time = dateutil.parser.parse(obj.cluster_update_time)
- return to_unix(update_time)
-
-
-class ClusterHealthCountersSerializer(serializers.Serializer):
- pg = serializers.SerializerMethodField('get_pg')
- mds = serializers.SerializerMethodField('get_mds')
- mon = serializers.SerializerMethodField('get_mon')
- osd = serializers.SerializerMethodField('get_osd')
-
- class Meta:
- fields = ('pg', 'mds', 'mon', 'osd', 'cluster_update_time', 'cluster_update_time_unix')
-
- def get_pg(self, obj):
- return obj.counters['pg']
-
- def get_mds(self, obj):
- return obj.counters['mds']
-
- def get_mon(self, obj):
- return obj.counters['mon']
-
- def get_osd(self, obj):
- return obj.counters['osd']
-
- # FIXME: should not be copying this field onto health counters etc, clients should get
- # it by querying the cluster directly.
- cluster_update_time = serializers.Field()
- cluster_update_time_unix = serializers.SerializerMethodField('get_cluster_update_time_unix')
-
- def get_cluster_update_time_unix(self, obj):
- update_time = dateutil.parser.parse(obj.cluster_update_time)
- return to_unix(update_time)
-
-
-class OSDDetailSerializer(serializers.Serializer):
- class Meta:
- # FIXME: should just be returning the OSD as the object
- fields = ('osd',)
-
- osd = serializers.Field()
-
-
-class OSDListSerializer(serializers.Serializer):
- # TODO: the OSD list resource should just return a list, so that
- # this serializer class isn't necessary
- osds = serializers.Field()
- pg_state_counts = serializers.SerializerMethodField('get_pg_state_counts')
-
- def get_pg_state_counts(self, obj):
- return dict((s, len(v)) for s, v in obj.osds_by_pg_state.iteritems())
-
- class Meta:
- fields = ('osds', 'pg_state_counts')
-
-
-class PoolSerializer(serializers.Serializer):
- class Meta:
- fields = ('pool_id', 'name', 'quota_max_bytes', 'quota_max_objects', 'used_objects', 'used_bytes', 'id', 'cluster')
-
- id = serializers.IntegerField()
- cluster = serializers.CharField()
- pool_id = serializers.IntegerField()
- name = serializers.CharField()
- quota_max_bytes = serializers.IntegerField()
- quota_max_objects = serializers.IntegerField()
- used_objects = serializers.IntegerField()
- used_bytes = serializers.IntegerField()
-
-
-class ServiceStatusSerializer(serializers.Serializer):
- class Meta:
- fields = ('type', 'service_id', 'name')
-
- type = serializers.SerializerMethodField('get_type')
- service_id = serializers.SerializerMethodField('get_service_id')
- name = serializers.SerializerMethodField('get_name')
-
- def get_type(self, obj):
- return obj['id'][1]
-
- def get_service_id(self, obj):
- return obj['id'][2]
-
- def get_name(self, obj):
- return "%s.%s" % (self.get_type(obj), self.get_service_id(obj))
-
-
-class ServerSerializer(serializers.Serializer):
- class Meta:
- fields = ('addr', 'hostname', 'name', 'services')
-
- services = ServiceStatusSerializer(source='services', many=True)
-
- addr = serializers.SerializerMethodField('get_addr')
- hostname = serializers.CharField()
- name = serializers.SerializerMethodField('get_name')
-
- def get_name(self, obj):
- return obj.hostname
-
- def get_addr(self, obj):
- return obj.fqdn
-
-
-class InfoSerializer(serializers.Serializer):
- class Meta:
- fields = ('version', 'license', 'registered', 'hostname', 'fqdn', 'ipaddr', 'bootstrap_url', 'bootstrap_rhel',
- 'bootstrap_ubuntu')
-
- version = serializers.CharField(help_text="Calamari server version")
- license = serializers.CharField(help_text="Calamari license metadata")
- registered = serializers.CharField(help_text="Calamari registration metadata")
- hostname = serializers.CharField(help_text="Hostname of Calamari server")
- fqdn = serializers.CharField(help_text="Fully qualified domain name of Calamari server")
- ipaddr = serializers.CharField(help_text="IP address of Calamari server")
- bootstrap_url = serializers.CharField(help_text="URL to minion bootstrap script")
- bootstrap_rhel = serializers.CharField(help_text="Minion bootstrap command line for Red Hat systems")
- bootstrap_ubuntu = serializers.CharField(help_text="Minion bootstrap command line for Ubuntu systems")
+
+from distutils.version import StrictVersion
+
+import rest_framework
from rest_framework import serializers
import rest.app.serializers.fields as fields
from rest.app.types import CRUSH_RULE_TYPE_REPLICATED, \
class ValidatingSerializer(serializers.Serializer):
- # django rest framework >= 3 renamed this field
@property
def init_data(self):
+ """
+ Compatibility alias for django rest framework 2 vs. 3
+ """
return self.initial_data
def is_valid(self, http_method):
- if False:
+ if StrictVersion(rest_framework.__version__) < StrictVersion("3.0.0"):
self._errors = super(ValidatingSerializer, self).errors or {}
else:
# django rest framework >= 3 has different is_Valid prototype
return filtered_data
-class ClusterSerializer(serializers.Serializer):
- class Meta:
- fields = ('update_time', 'id', 'name')
-
- update_time = serializers.DateTimeField(
- help_text="The time at which the last status update from this cluster was received"
- )
- name = serializers.Field(
- help_text="Human readable cluster name, not a unique identifier"
- )
- id = serializers.Field(
- help_text="The FSID of the cluster, universally unique"
- )
-
-
class PoolSerializer(ValidatingSerializer):
class Meta:
fields = ('name', 'id', 'size', 'pg_num', 'crush_ruleset', 'min_size',
help_text="Time at which the request completed, may be null.")
-class SaltKeySerializer(ValidatingSerializer):
- class Meta:
- fields = ('id', 'status')
- create_allowed = ()
- create_required = ()
- modify_allowed = ('status',)
- modify_required = ()
-
- id = serializers.CharField(required=False,
- help_text="The minion ID, usually equal to a host's FQDN")
- status = serializers.CharField(
- help_text="One of 'accepted', 'rejected' or 'pre'")
-
-
class ServiceSerializer(serializers.Serializer):
class Meta:
fields = ('type', 'id')
help_text="List of Ceph services seen"
"on this server")
- # Ceph network configuration
- # frontend_addr = serializers.CharField() # may be null if no OSDs or mons on server
- # backend_addr = serializers.CharField() # may be null if no OSDs on server
-
- # TODO: reinstate by having OSDs resolve addresses to ifaces and report
- # in their metadata
- # frontend_iface = serializers.CharField() # may be null if interface for frontend addr not up
- # backend_iface = serializers.CharField() # may be null if interface for backend addr not up
-
-
-class EventSerializer(serializers.Serializer):
- class Meta:
- fields = ('when', 'severity', 'message')
-
- when = serializers.DateTimeField(
- help_text="Time at which event was generated")
- severity = serializers.SerializerMethodField('get_severity')
- message = serializers.CharField(
- help_text="One line human readable description")
-
- def get_severity(self, obj):
- return severity_str(obj.severity)
-
-
-class LogTailSerializer(serializers.Serializer):
- """
- Trivial serializer to wrap a string blob of log output
- """
-
- class Meta:
- fields = ('lines',)
-
- lines = serializers.CharField(
- help_text="Retrieved log data as a newline-separated string")
-
class ConfigSettingSerializer(serializers.Serializer):
class Meta:
# Declarative metaclass definitions are great until you want
# to use a reserved word
-if False:
+if StrictVersion(rest_framework.__version__) < StrictVersion("3.0.0"):
# In django-rest-framework 2.3.x (Calamari used this)
OsdSerializer.base_fields['in'] = OsdSerializer.base_fields['_in']
OsdConfigSerializer.base_fields['nodeep-scrub'] = \
OsdConfigSerializer.base_fields['nodeepscrub']
- # django_rest_framework 2.3.12 doesn't let me put help_text on a methodfield
- # https://github.com/tomchristie/django-rest-framework/pull/1594
- EventSerializer.base_fields['severity'].help_text = "One of %s" % ",".join(
- SEVERITIES.values())
else:
OsdSerializer._declared_fields['in'] = OsdSerializer._declared_fields[
'_in']
+ del OsdSerializer._declared_fields['_in']
+ OsdSerializer._declared_fields['in'].source = "in"
OsdConfigSerializer._declared_fields['nodeep-scrub'] = \
OsdConfigSerializer._declared_fields['nodeepscrub']
- EventSerializer._declared_fields[
- 'severity'].help_text = "One of %s" % ",".join(SEVERITIES.values())
+
+
router = routers.DefaultRouter(trailing_slash=False)
-# Information about each Ceph cluster (FSID), see sub-URLs
-
urlpatterns = patterns(
'',
- # About the host calamari server is running on
- # url(r'^grains', rest.app.views.v2.grains),
-
# This has to come after /user/me to make sure that special case is handled
url(r'^', include(router.urls)),
rest.app.views.v2.ConfigViewSet.as_view({'get': 'list'})),
url(r'^cluster/config/(?P<key>[a-zA-Z0-9_]+)$',
rest.app.views.v2.ConfigViewSet.as_view({'get': 'retrieve'})),
-
- # Events
- # url(r'^event$', rest.app.views.v2.EventViewSet.as_view({'get': 'list'})),
- # url(r'^cluster/event$', rest.app.views.v2.EventViewSet.as_view({'get': 'list_cluster'})),
- # url(r'^server/(?P<fqdn>[a-zA-Z0-9-\.]+)/event$', rest.app.views.v2.EventViewSet.as_view({'get': 'list_server'})),
-
- # Log tail
- # url(r'^cluster/log$',
- # rest.app.views.v2.LogTailViewSet.as_view({'get': 'get_cluster_log'})),
- # url(r'^server/(?P<fqdn>[a-zA-Z0-9-\.]+)/log$',
- # rest.app.views.v2.LogTailViewSet.as_view({'get': 'list_server_logs'})),
- # url(r'^server/(?P<fqdn>[a-zA-Z0-9-\.]+)/log/(?P<log_path>.+)$',
- # rest.app.views.v2.LogTailViewSet.as_view({'get': 'get_server_log'})),
-
- # Ceph CLI access
- # url(r'^cluster/cli$',
- # rest.app.views.v2.CliViewSet.as_view({'post': 'create'}))
)
'error_message': request.error_message,
'status': request.status,
'headline': request.headline,
- 'requested_at': request.requested_at.isoformat(),
- 'completed_at': request.completed_at.isoformat() if request.completed_at else None
+ 'requested_at': request.requested_at,
+ 'completed_at': request.completed_at
}
def get_request(self, request_id):
import json
import logging
import shlex
+from distutils.version import StrictVersion
from django.http import Http404
+import rest_framework
from rest_framework.exceptions import ParseError, APIException, PermissionDenied
from rest_framework.response import Response
from rest_framework.decorators import api_view
from django.contrib.auth.decorators import login_required
-from rest.app.serializers.v2 import PoolSerializer, CrushRuleSetSerializer, CrushRuleSerializer, \
- ServerSerializer, SaltKeySerializer, RequestSerializer, \
- ClusterSerializer, EventSerializer, LogTailSerializer, OsdSerializer, ConfigSettingSerializer, MonSerializer, OsdConfigSerializer, \
- CliSerializer
-#from rest.app.views.database_view_set import DatabaseViewSet
+from rest.app.serializers.v2 import PoolSerializer, CrushRuleSetSerializer, \
+ CrushRuleSerializer, ServerSerializer, RequestSerializer, OsdSerializer, \
+ ConfigSettingSerializer, MonSerializer, OsdConfigSerializer
from rest.app.views.exceptions import ServiceUnavailable
-#from rest.app.views.paginated_mixin import PaginatedMixin
-#from rest.app.views.remote_view_set import RemoteViewSet
from rest.app.views.rpc_view import RPCViewSet, DataObject
-from rest.app.types import CRUSH_RULE, POOL, OSD, USER_REQUEST_COMPLETE, USER_REQUEST_SUBMITTED, \
- OSD_IMPLEMENTED_COMMANDS, MON, OSD_MAP, SYNC_OBJECT_TYPES, ServiceId, severity_from_str, SEVERITIES, \
+from rest.app.types import CRUSH_RULE, POOL, OSD, USER_REQUEST_COMPLETE, \
+ USER_REQUEST_SUBMITTED, OSD_IMPLEMENTED_COMMANDS, MON, OSD_MAP, \
+ SYNC_OBJECT_TYPES, ServiceId, severity_from_str, SEVERITIES, \
OsdMap, Config, MonMap, MonStatus
-class Event(object):
- pass
-
from rest.logger import logger
log = logger()
raise ParseError("State must be one of %s" % ", ".join(valid_states))
requests = self.client.list_requests({'state': filter_state, 'fsid': fsid})
- if False:
- # FIXME reinstate pagination, broke in DRF 2.x -> 3.x
+ if StrictVersion(rest_framework.__version__) < StrictVersion("3.0.0"):
return Response(self._paginate(request, requests))
else:
+ # FIXME reinstate pagination, broke in DRF 2.x -> 3.x
return Response(requests)
return Response(CrushRuleSetSerializer(rulesets, many=True).data)
-class SaltKeyViewSet(RPCViewSet):
- """
-Ceph servers authentication with the Calamari using a key pair. Before
-Calamari accepts messages from a server, the server's key must be accepted.
- """
- serializer_class = SaltKeySerializer
-
- def list(self, request):
- return Response(self.serializer_class(self.client.minion_status(None), many=True).data)
-
- def partial_update(self, request, minion_id):
- serializer = self.serializer_class(data=request.DATA)
- if serializer.is_valid(request.method):
- self._partial_update(minion_id, serializer.get_data())
- return Response(status=status.HTTP_204_NO_CONTENT)
- else:
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
- def _partial_update(self, minion_id, data):
- valid_status = ['accepted', 'rejected']
- if 'status' not in data:
- raise ParseError({'status': "This field is mandatory"})
- elif data['status'] not in valid_status:
- raise ParseError({'status': "Must be one of %s" % ",".join(valid_status)})
- else:
- key = self.client.minion_get(minion_id)
- transition = [key['status'], data['status']]
- if transition == ['pre', 'accepted']:
- self.client.minion_accept(minion_id)
- elif transition == ['pre', 'rejected']:
- self.client.minion_reject(minion_id)
- else:
- raise ParseError({'status': ["Transition {0}->{1} is invalid".format(
- transition[0], transition[1]
- )]})
-
- def _validate_list(self, request):
- keys = request.DATA
- if not isinstance(keys, list):
- raise ParseError("Bulk PATCH must send a list")
- for key in keys:
- if 'id' not in key:
- raise ParseError("Items in bulk PATCH must have 'id' attribute")
-
- def list_partial_update(self, request):
- self._validate_list(request)
-
- keys = request.DATA
- log.debug("KEYS %s" % keys)
- for key in keys:
- self._partial_update(key['id'], key)
-
- return Response(status=status.HTTP_204_NO_CONTENT)
-
- def destroy(self, request, minion_id):
- self.client.minion_delete(minion_id)
- return Response(status=status.HTTP_204_NO_CONTENT)
-
- def list_destroy(self, request):
- self._validate_list(request)
- keys = request.DATA
- for key in keys:
- self.client.minion_delete(key['id'])
-
- return Response(status=status.HTTP_204_NO_CONTENT)
-
- def retrieve(self, request, minion_id):
- return Response(self.serializer_class(self.client.minion_get(minion_id)).data)
-
-
class PoolDataObject(DataObject):
"""
Slightly dressed up version of the raw pool from osd dump
many=True).data)
-if False:
- class EventViewSet(DatabaseViewSet, PaginatedMixin):
- """
- Events generated by Calamari server in response to messages from
- servers and Ceph clusters. This resource is paginated.
-
- Note that events are not visible synchronously with respect to
- all other API resources. For example, you might read the OSD
- map, see an OSD is down, then quickly read the events and find
- that the event about the OSD going down is not visible yet (though
- it would appear very soon after).
-
- The ``severity`` attribute mainly follows a typical INFO, WARN, ERROR
- hierarchy. However, we have an additional level between INFO and WARN
- called RECOVERY. Where something going bad in the system is usually
- a WARN message, the opposite state transition is usually a RECOVERY
- message.
-
- This resource supports "more severe than" filtering on the severity
- attribute. Pass the desired severity threshold as a URL parameter
- in a GET, such as ``?severity=RECOVERY`` to show everything but INFO.
-
- """
- serializer_class = EventSerializer
-
- @property
- def queryset(self):
- return self.session.query(Event).order_by(Event.when.desc())
-
- def _filter_by_severity(self, request, queryset=None):
- if queryset is None:
- queryset = self.queryset
- severity_str = request.GET.get("severity", "INFO")
- try:
- severity = severity_from_str(severity_str)
- except KeyError:
- raise ParseError("Invalid severity '%s', must be on of %s" % (severity_str,
- ",".join(SEVERITIES.values())))
-
- return queryset.filter(Event.severity <= severity)
-
- def list(self, request):
- return Response(self._paginate(request, self._filter_by_severity(request)))
-
- def list_cluster(self, request):
- return Response(self._paginate(request, self._filter_by_severity(request, self.queryset.filter_by(fsid=fsid))))
-
- def list_server(self, request, fqdn):
- return Response(self._paginate(request, self._filter_by_severity(request, self.queryset.filter_by(fqdn=fqdn))))
-
-
-if False:
- class LogTailViewSet(RemoteViewSet):
- """
- A primitive remote log viewer.
-
- Logs are retrieved on demand from the Ceph servers, so this resource will return a 503 error if no suitable
- server is available to get the logs.
-
- GETs take an optional ``lines`` parameter for the number of lines to retrieve.
- """
- serializer_class = LogTailSerializer
-
- def get_cluster_log(self, request):
- """
- Retrieve the cluster log from one of a cluster's mons (expect it to be in /var/log/ceph/ceph.log)
- """
-
- # Number of lines to get
- lines = request.GET.get('lines', 40)
-
- # Resolve FSID to name
- name = self.client.get_cluster(fsid)['name']
-
- # Execute remote operation synchronously
- result = self.run_mon_job("log_tail.tail", ["ceph/{name}.log".format(name=name), lines])
-
- return Response({'lines': result})
-
- def list_server_logs(self, request, fqdn):
- return Response(sorted(self.run_job(fqdn, "log_tail.list_logs", ["."])))
-
- def get_server_log(self, request, fqdn, log_path):
- lines = request.GET.get('lines', 40)
- return Response({'lines': self.run_job(fqdn, "log_tail.tail", [log_path, lines])})
-
-
class MonViewSet(RPCViewSet):
"""
Ceph monitor services.
self.serializer_class([DataObject(m) for m in mons],
many=True).data)
-
-if False:
- class CliViewSet(RemoteViewSet):
- """
- Access the `ceph` CLI tool remotely.
-
- To achieve the same result as running "ceph osd dump" at a shell, an
- API consumer may POST an object in either of the following formats:
-
- ::
-
- {'command': ['osd', 'dump']}
-
- {'command': 'osd dump'}
-
-
- The response will be a 200 status code if the command executed, regardless
- of whether it was successful, to check the result of the command itself
- read the ``status`` attribute of the returned data.
-
- The command will be executed on the first available mon server, retrying
- on subsequent mon servers if no response is received. Due to this retry
- behaviour, it is possible for the command to be run more than once in
- rare cases; since most ceph commands are idempotent this is usually
- not a problem.
- """
- serializer_class = CliSerializer
-
- def create(self, request):
- # Validate
- try:
- command = request.DATA['command']
- except KeyError:
- raise ParseError("'command' field is required")
- else:
- if not (isinstance(command, basestring) or isinstance(command, list)):
- raise ParseError("'command' must be a string or list")
-
- # Parse string commands to list
- if isinstance(command, basestring):
- command = shlex.split(command)
-
- name = self.client.get_cluster(fsid)['name']
- result = self.run_mon_job("ceph.ceph_command", [name, command])
- log.debug("CliViewSet: result = '%s'" % result)
-
- if not isinstance(result, dict):
- # Errors from salt like "module not available" come back as strings
- raise APIException("Remote error: %s" % str(result))
-
- return Response(self.serializer_class(DataObject(result)).data)