#   Copyright (c) 2018 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from django.http import HttpResponseRedirect, HttpResponse, JsonResponse, HttpResponseNotFound, HttpResponseForbidden
from django.contrib import messages
from django.conf import settings
from django.utils.encoding import smart_str
from django.contrib.auth.decorators import login_required
from kubernetes import client, config
from kubernetes.client.rest import ApiException
from . import PaddleJob
from rest_framework.authtoken.models import Token
from rest_framework import viewsets, generics, permissions
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.parsers import MultiPartParser, FormParser, FileUploadParser
import json
import utils
import notebook.utils
import logging
import os
import copy
from notebook.models import FilePublish
import uuid
from cloudprovider.k8s_provider import K8sProvider
from paddle_job import PaddleJob


def file_publish_view(request):
    """
        view for download published files
    """
    username = request.user.username
    publish_uuid = request.GET.get("uuid")
    if not publish_uuid:
        return HttpResponseNotFound()
    record = FilePublish.objects.get(uuid=publish_uuid)
    if not record:
        return HttpResponseNotFound()
    # FIXME(typhoonzero): not support folder currently
    if record.path.endswith("/"):
        return HttpResponseNotFound()

    real_path = "/".join([settings.STORAGE_PATH] + record.path.split("/")[4:])
    logging.info("downloading file from: %s, record(%s)", real_path,
                 record.path)

    # mimetype is replaced by content_type for django 1.7
    response = HttpResponse(
        open(real_path), content_type='application/force-download')
    response[
        'Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(
            record.path)
    # It's usually a good idea to set the 'Content-Length' header too.
    # You can also set any other required headers: Cache-Control, etc.
    return response


class FilePublishAPIView(APIView):
    permission_classes = (permissions.IsAuthenticated, )

    def get(self, request, format=None):
        """
            return a list of published files for current user
        """
        record = FilePublish.objects.filter(user=request.user)
        file_list = [rec.path for rec in record]
        url_list = [rec.url for rec in record]
        return Response({"files": file_list, "urls": url_list})

    def post(self, request, format=None):
        """
            given a pfs path generate a uniq sharing url for the path
        """
        post_body = json.loads(request.body)
        file_path = post_body.get("path")
        publish_uuid = uuid.uuid4()
        publish_url = "http://%s/filepub/?uuid=%s" % (
            request.META["HTTP_HOST"], publish_uuid)
        # save publish_url to mysql
        publish_record = FilePublish()
        publish_record.url = publish_url
        publish_record.user = request.user
        publish_record.path = file_path
        publish_record.uuid = publish_uuid
        publish_record.save()
        return Response({"url": publish_url})


def get_paddlejob(request):
    username = request.user.username
    obj = json.loads(request.body)
    topology = obj.get("topology", "")
    entry = obj.get("entry", "")
    if not topology and not entry:
        return utils.simple_response(500, "no topology or entry specified")
    if not obj.get("datacenter"):
        return utils.simple_response(500, "no datacenter specified")
    cfgs = {}
    dc = obj.get("datacenter")
    # jobPackage validation: startwith /pfs
    # NOTE: job packages are uploaded to /pfs/[dc]/home/[user]/jobs/[jobname]
    job_name = obj.get("name", "paddle-cluster-job")
    package_in_pod = os.path.join("/pfs/%s/home/%s" % (dc, username), "jobs",
                                  job_name)

    logging.info("current package: %s", package_in_pod)
    # package must be ready before submit a job
    package_path_4test = package_in_pod.replace("/pfs/%s/home" % dc,
                                                settings.STORAGE_PATH)
    if not os.path.exists(package_path_4test):
        package_path_4test = package_in_pod.replace("/pfs/%s/home/%s" % (
            dc, username), settings.STORAGE_PATH)
        if not os.path.exists(package_path_4test):
            return utils.error_message_response(
                "package not exist in cloud: %s" % package_path_4test)
    logging.info("current package in pod: %s", package_path_4test)

    envs = {}
    envs.update({"PADDLE_CLOUD_CURRENT_DATACENTER": dc})
    envs.update({"PADDLE_CLOUD_USERNAME": username})
    # ===================== create PaddleJob instance ======================
    paddle_job = PaddleJob(
        name=job_name,
        job_package=package_in_pod,
        parallelism=obj.get("parallelism", 1),
        cpu=obj.get("cpu", 1),
        memory=obj.get("memory", "1Gi"),
        pservers=obj.get("pservers", 1),
        pscpu=obj.get("pscpu", 1),
        psmemory=obj.get("psmemory", "1Gi"),
        topology=topology,
        entry=entry,
        gpu=obj.get("gpu", 0),
        image=obj.get("image", None),
        passes=obj.get("passes", 1),
        registry_secret=obj.get("registry", None),
        volumes=[],
        envs=envs,
        fault_tolerant=obj.get("faulttolerant", False),
        min_instance=obj.get("min_instance", 1),
        max_instance=obj.get("max_instance", 1),
        etcd_image=settings.ETCD_IMAGE,
        dc=dc)

    logging.info("return paddlejob")
    return paddle_job


class TrainingJobsView(APIView):
    permission_classes = (permissions.IsAuthenticated, )

    def get(self, request, format=None):
        username = request.user.username
        p = K8sProvider()
        ret_dict = p.get_trainingjobs(username)
        return Response(ret_dict)

    def post(self, request, format=None):
        """
        Submit a trainingjobs.
        """
        username = request.user.username
        job = get_paddlejob(request)
        p = K8sProvider()
        try:
            p.submit_trainingjobs(job, username)
        except Exception, e:
            return utils.error_message_response(str(e))

        return utils.simple_response(200, "")

    def delete(self, request, format=None):
        """
        Kill a trainingjobs.
        """
        username = request.user.username
        obj = json.loads(request.body)
        jobname = obj.get("jobname")

        p = K8sProvider()
        retcode, status = p.delete_trainingjobs(jobname, username)
        return utils.simple_response(retcode, "\n".join(status))


class JobsView(APIView):
    permission_classes = (permissions.IsAuthenticated, )

    def get(self, request, format=None):
        username = request.user.username
        p = K8sProvider()
        ret_dict = p.get_jobs(username)
        return Response(ret_dict)

    def post(self, request, format=None):
        """
        Submit the PaddlePaddle job
        """
        # ========== submit master ReplicaSet if using fault_tolerant feature ==
        username = request.user.username
        job = get_paddlejob(request)
        p = K8sProvider()
        try:
            p.submit_job(job, username)
        except Exception, e:
            return utils.error_message_response(str(e))

        return utils.simple_response(200, "")

    def delete(self, request, format=None):
        """
        Kill a job
        """
        username = request.user.username
        obj = json.loads(request.body)
        jobname = obj.get("jobname")
        p = K8sProvider()
        retcode, status = p.delete_job(jobname, username)
        return utils.simple_response(retcode, "\n".join(status))


class PserversView(APIView):
    permission_classes = (permissions.IsAuthenticated, )

    def get(self, request, format=None):
        """
        List all pservers
        """
        username = request.user.username
        p = K8sProvider()
        return Response(p.get_pservers(username))


class LogsView(APIView):
    permission_classes = (permissions.IsAuthenticated, )

    def get(self, request, format=None):
        username = request.user.username
        jobname = request.query_params.get("jobname")
        num_lines = request.query_params.get("n")
        worker = request.query_params.get("w")

        total_job_log = K8sProvider().get_logs(jobname, num_lines, worker,
                                               username)
        return utils.simple_response(200, total_job_log)


class WorkersView(APIView):
    permission_classes = (permissions.IsAuthenticated, )

    def get(self, request, format=None):
        username = request.user.username
        jobname = request.query_params.get("jobname")
        ret = K8sProvider().get_workers(jobname, username)
        return Response(ret)


class QuotaView(APIView):
    permission_classes = (permissions.IsAuthenticated, )

    def get(self, request, format=None):
        username = request.user.username
        ret = K8sProvider().get_quotas(username)
        return Response(ret)


class GetUserView(APIView):
    permission_classes = (permissions.IsAuthenticated, )

    def get(self, request, format=None):
        """
        Get user name
        """
        content = {
            'user':
            request.user.username,  # `django.contrib.auth.User` instance.
        }
        return Response(content)


class SimpleFileView(APIView):
    permission_classes = (permissions.IsAuthenticated, )
    parser_classes = (
        FormParser,
        MultiPartParser, )

    def __validate_path(self, request, file_path):
        """
        returns error_msg. error_msg will be empty if there's no error.
        """
        path_parts = file_path.split(os.path.sep)

        assert (path_parts[1] == "pfs")
        assert (path_parts[2] in settings.DATACENTERS.keys())
        assert (path_parts[3] == "home")
        assert (path_parts[4] == request.user.username)

        server_file = os.path.join(settings.STORAGE_PATH,
                                   request.user.username, *path_parts[5:])

        return server_file

    def get(self, request, format=None):
        """
        Simple get file.
        """
        file_path = request.query_params.get("path")
        try:
            write_file = self.__validate_path(request, file_path)
        except Exception, e:
            return utils.error_message_response("file path not valid: %s" %
                                                str(e))

        if not os.path.exists(os.sep + write_file):
            return Response({"msg": "file not exist"})

        response = HttpResponse(
            open(write_file), content_type='application/force-download')
        response[
            'Content-Disposition'] = 'attachment; filename="%s"' % os.path.basename(
                write_file)

        return response

    def post(self, request, format=None):
        """
        Simple put file.
        """
        file_obj = request.data['file']
        file_path = request.query_params.get("path")
        if not file_path:
            return utils.error_message_response("must specify path")
        try:
            write_file = self.__validate_path(request, file_path)
        except Exception, e:
            return utils.error_message_response("file path not valid: %s" %
                                                str(e))

        if not os.path.exists(os.path.dirname(write_file)):
            try:
                os.makedirs(os.path.dirname(write_file))
            except OSError as exc:  # Guard against race condition
                if exc.errno != errno.EEXIST:
                    raise
        # FIXME: always overwrite package files
        with open(write_file, "w") as fn:
            while True:
                data = file_obj.read(4096)
                if not data:
                    break
                fn.write(data)

        return Response({"msg": ""})


class SimpleFileList(APIView):
    permission_classes = (permissions.IsAuthenticated, )
    parser_classes = (
        FormParser,
        MultiPartParser, )

    def get(self, request, format=None):
        """
        Simple list files.
        """
        file_path = request.query_params.get("path")
        dc = request.query_params.get("dc")
        # validate list path must be under user's dir
        path_parts = file_path.split(os.path.sep)
        msg = ""
        if len(path_parts) < 5:
            msg = "path must like /pfs/[dc]/home/[user]"
        else:
            if path_parts[1] != "pfs":
                msg = "path must start with /pfs"
            if path_parts[2] not in settings.DATACENTERS.keys():
                msg = "no datacenter " + path_parts[2]
            if path_parts[3] != "home":
                msg = "path must like /pfs/[dc]/home/[user]"
            if path_parts[4] != request.user.username:
                msg = "not a valid user: " + path_parts[4]
        if msg:
            return Response({"msg": msg})

        real_path = file_path.replace("/pfs/%s/home/%s" % (
            dc, request.user.username), "/pfs/%s" % request.user.username)
        if not os.path.exists(real_path):
            return Response({"msg": "dir not exist"})

        return Response({"msg": "", "items": os.listdir(real_path)})

#   Copyright (c) 2018 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from django.http import HttpResponseRedirect, HttpResponse, JsonResponse, HttpResponseNotFound, HttpResponseForbidden
from django.contrib import messages
from django.conf import settings
from django.utils.encoding import smart_str
from django.contrib.auth.decorators import login_required
from kubernetes import client, config
from kubernetes.client.rest import ApiException
from . import PaddleJob
from rest_framework.authtoken.models import Token
from rest_framework import viewsets, generics, permissions
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.parsers import MultiPartParser, FormParser, FileUploadParser
import json
import utils
import notebook.utils
import logging
import os
import copy
from notebook.models import FilePublish
import uuid
from cloudprovider.k8s_provider import K8sProvider
from paddle_job import PaddleJob


def file_publish_view(request):
    """
        view for download published files
    """
    username = request.user.username
    publish_uuid = request.GET.get("uuid")
    if not publish_uuid:
        return HttpResponseNotFound()
    record = FilePublish.objects.get(uuid=publish_uuid)
    if not record:
        return HttpResponseNotFound()
    # FIXME(typhoonzero): not support folder currently
    if record.path.endswith("/"):
        return HttpResponseNotFound()

    real_path = "/".join([settings.STORAGE_PATH] + record.path.split("/")[4:])
    logging.info("downloading file from: %s, record(%s)", real_path,
                 record.path)

    # mimetype is replaced by content_type for django 1.7
    response = HttpResponse(
        open(real_path), content_type='application/force-download')
    response[
        'Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(
            record.path)
    # It's usually a good idea to set the 'Content-Length' header too.
    # You can also set any other required headers: Cache-Control, etc.
    return response


class FilePublishAPIView(APIView):
    permission_classes = (permissions.IsAuthenticated, )

    def get(self, request, format=None):
        """
            return a list of published files for current user
        """
        record = FilePublish.objects.filter(user=request.user)
        file_list = [rec.path for rec in record]
        url_list = [rec.url for rec in record]
        return Response({"files": file_list, "urls": url_list})

    def post(self, request, format=None):
        """
            given a pfs path generate a uniq sharing url for the path
        """
        post_body = json.loads(request.body)
        file_path = post_body.get("path")
        publish_uuid = uuid.uuid4()
        publish_url = "http://%s/filepub/?uuid=%s" % (
            request.META["HTTP_HOST"], publish_uuid)
        # save publish_url to mysql
        publish_record = FilePublish()
        publish_record.url = publish_url
        publish_record.user = request.user
        publish_record.path = file_path
        publish_record.uuid = publish_uuid
        publish_record.save()
        return Response({"url": publish_url})


def get_paddlejob(request):
    username = request.user.username
    obj = json.loads(request.body)
    topology = obj.get("topology", "")
    entry = obj.get("entry", "")
    if not topology and not entry:
        return utils.simple_response(500, "no topology or entry specified")
    if not obj.get("datacenter"):
        return utils.simple_response(500, "no datacenter specified")
    cfgs = {}
    dc = obj.get("datacenter")
    # jobPackage validation: startwith /pfs
    # NOTE: job packages are uploaded to /pfs/[dc]/home/[user]/jobs/[jobname]
    job_name = obj.get("name", "paddle-cluster-job")
    package_in_pod = os.path.join("/pfs/%s/home/%s" % (dc, username), "jobs",
                                  job_name)

    logging.info("current package: %s", package_in_pod)
    # package must be ready before submit a job
    package_path_4test = package_in_pod.replace("/pfs/%s/home" % dc,
                                                settings.STORAGE_PATH)
    if not os.path.exists(package_path_4test):
        package_path_4test = package_in_pod.replace("/pfs/%s/home/%s" % (
            dc, username), settings.STORAGE_PATH)
        if not os.path.exists(package_path_4test):
            return utils.error_message_response(
                "package not exist in cloud: %s" % package_path_4test)
    logging.info("current package in pod: %s", package_path_4test)

    envs = {}
    envs.update({"PADDLE_CLOUD_CURRENT_DATACENTER": dc})
    envs.update({"PADDLE_CLOUD_USERNAME": username})
    # ===================== create PaddleJob instance ======================
    paddle_job = PaddleJob(
        name=job_name,
        job_package=package_in_pod,
        parallelism=obj.get("parallelism", 1),
        cpu=obj.get("cpu", 1),
        memory=obj.get("memory", "1Gi"),
        pservers=obj.get("pservers", 1),
        pscpu=obj.get("pscpu", 1),
        psmemory=obj.get("psmemory", "1Gi"),
        topology=topology,
        entry=entry,
        gpu=obj.get("gpu", 0),
        image=obj.get("image", None),
        passes=obj.get("passes", 1),
        registry_secret=obj.get("registry", None),
        volumes=[],
        envs=envs,
        fault_tolerant=obj.get("faulttolerant", False),
        min_instance=obj.get("min_instance", 1),
        max_instance=obj.get("max_instance", 1),
        etcd_image=settings.ETCD_IMAGE,
        dc=dc)

    logging.info("return paddlejob")
    return paddle_job


class TrainingJobsView(APIView):
    permission_classes = (permissions.IsAuthenticated, )

    def get(self, request, format=None):
        username = request.user.username
        p = K8sProvider()
        ret_dict = p.get_trainingjobs(username)
        return Response(ret_dict)

    def post(self, request, format=None):
        """
        Submit a trainingjobs.
        """
        username = request.user.username
        job = get_paddlejob(request)
        p = K8sProvider()
        try:
            p.submit_trainingjobs(job, username)
        except Exception, e:
            return utils.error_message_response(str(e))

        return utils.simple_response(200, "")

    def delete(self, request, format=None):
        """
        Kill a trainingjobs.
        """
        username = request.user.username
        obj = json.loads(request.body)
        jobname = obj.get("jobname")

        p = K8sProvider()
        retcode, status = p.delete_trainingjobs(jobname, username)
        return utils.simple_response(retcode, "\n".join(status))


class JobsView(APIView):
    permission_classes = (permissions.IsAuthenticated, )

    def get(self, request, format=None):
        username = request.user.username
        p = K8sProvider()
        ret_dict = p.get_jobs(username)
        return Response(ret_dict)

    def post(self, request, format=None):
        """
        Submit the PaddlePaddle job
        """
        # ========== submit master ReplicaSet if using fault_tolerant feature ==
        username = request.user.username
        job = get_paddlejob(request)
        p = K8sProvider()
        try:
            p.submit_job(job, username)
        except Exception, e:
            return utils.error_message_response(str(e))

        return utils.simple_response(200, "")

    def delete(self, request, format=None):
        """
        Kill a job
        """
        username = request.user.username
        obj = json.loads(request.body)
        jobname = obj.get("jobname")
        p = K8sProvider()
        retcode, status = p.delete_job(jobname, username)
        return utils.simple_response(retcode, "\n".join(status))


class PserversView(APIView):
    permission_classes = (permissions.IsAuthenticated, )

    def get(self, request, format=None):
        """
        List all pservers
        """
        username = request.user.username
        p = K8sProvider()
        return Response(p.get_pservers(username))


class LogsView(APIView):
    permission_classes = (permissions.IsAuthenticated, )

    def get(self, request, format=None):
        username = request.user.username
        jobname = request.query_params.get("jobname")
        num_lines = request.query_params.get("n")
        worker = request.query_params.get("w")

        total_job_log = K8sProvider().get_logs(jobname, num_lines, worker,
                                               username)
        return utils.simple_response(200, total_job_log)


class WorkersView(APIView):
    permission_classes = (permissions.IsAuthenticated, )

    def get(self, request, format=None):
        username = request.user.username
        jobname = request.query_params.get("jobname")
        ret = K8sProvider().get_workers(jobname, username)
        return Response(ret)


class QuotaView(APIView):
    permission_classes = (permissions.IsAuthenticated, )

    def get(self, request, format=None):
        username = request.user.username
        ret = K8sProvider().get_quotas(username)
        return Response(ret)


class GetUserView(APIView):
    permission_classes = (permissions.IsAuthenticated, )

    def get(self, request, format=None):
        """
        Get user name
        """
        content = {
            'user':
            request.user.username,  # `django.contrib.auth.User` instance.
        }
        return Response(content)


class SimpleFileView(APIView):
    permission_classes = (permissions.IsAuthenticated, )
    parser_classes = (
        FormParser,
        MultiPartParser, )

    def __validate_path(self, request, file_path):
        """
        returns error_msg. error_msg will be empty if there's no error.
        """
        path_parts = file_path.split(os.path.sep)

        assert (path_parts[1] == "pfs")
        assert (path_parts[2] in settings.DATACENTERS.keys())
        assert (path_parts[3] == "home")
        assert (path_parts[4] == request.user.username)

        server_file = os.path.join(settings.STORAGE_PATH,
                                   request.user.username, *path_parts[5:])

        return server_file

    def get(self, request, format=None):
        """
        Simple get file.
        """
        file_path = request.query_params.get("path")
        try:
            write_file = self.__validate_path(request, file_path)
        except Exception, e:
            return utils.error_message_response("file path not valid: %s" %
                                                str(e))

        if not os.path.exists(os.sep + write_file):
            return Response({"msg": "file not exist"})

        response = HttpResponse(
            open(write_file), content_type='application/force-download')
        response[
            'Content-Disposition'] = 'attachment; filename="%s"' % os.path.basename(
                write_file)

        return response

    def post(self, request, format=None):
        """
        Simple put file.
        """
        file_obj = request.data['file']
        file_path = request.query_params.get("path")
        if not file_path:
            return utils.error_message_response("must specify path")
        try:
            write_file = self.__validate_path(request, file_path)
        except Exception, e:
            return utils.error_message_response("file path not valid: %s" %
                                                str(e))

        if not os.path.exists(os.path.dirname(write_file)):
            try:
                os.makedirs(os.path.dirname(write_file))
            except OSError as exc:  # Guard against race condition
                if exc.errno != errno.EEXIST:
                    raise
        # FIXME: always overwrite package files
        with open(write_file, "w") as fn:
            while True:
                data = file_obj.read(4096)
                if not data:
                    break
                fn.write(data)

        return Response({"msg": ""})


class SimpleFileList(APIView):
    permission_classes = (permissions.IsAuthenticated, )
    parser_classes = (
        FormParser,
        MultiPartParser, )

    def get(self, request, format=None):
        """
        Simple list files.
        """
        file_path = request.query_params.get("path")
        dc = request.query_params.get("dc")
        # validate list path must be under user's dir
        path_parts = file_path.split(os.path.sep)
        msg = ""
        if len(path_parts) < 5:
            msg = "path must like /pfs/[dc]/home/[user]"
        else:
            if path_parts[1] != "pfs":
                msg = "path must start with /pfs"
            if path_parts[2] not in settings.DATACENTERS.keys():
                msg = "no datacenter " + path_parts[2]
            if path_parts[3] != "home":
                msg = "path must like /pfs/[dc]/home/[user]"
            if path_parts[4] != request.user.username:
                msg = "not a valid user: " + path_parts[4]
        if msg:
            return Response({"msg": msg})

        real_path = file_path.replace("/pfs/%s/home/%s" % (
            dc, request.user.username), "/pfs/%s" % request.user.username)
        if not os.path.exists(real_path):
            return Response({"msg": "dir not exist"})

        return Response({"msg": "", "items": os.listdir(real_path)})

#   Copyright (c) 2018 PaddlePaddle Authors. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # #     http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. view for download published files # FIXME(typhoonzero): not support folder currently # mimetype is replaced by content_type for django 1.7 # It's usually a good idea to set the 'Content-Length' header too. # You can also set any other required headers: Cache-Control, etc. return a list of published files for current user given a pfs path generate a uniq sharing url for the path # save publish_url to mysql # jobPackage validation: startwith /pfs # NOTE: job packages are uploaded to /pfs/[dc]/home/[user]/jobs/[jobname] # package must be ready before submit a job # ===================== create PaddleJob instance ====================== Submit a trainingjobs. Kill a trainingjobs. Submit the PaddlePaddle job # ========== submit master ReplicaSet if using fault_tolerant feature == Kill a job List all pservers Get user name # `django.contrib.auth.User` instance. returns error_msg. error_msg will be empty if there's no error. Simple get file. Simple put file. # Guard against race condition # FIXME: always overwrite package files Simple list files. # validate list path must be under user's dir