1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# BAREOS - Backup Archiving REcovery Open Sourced
4#
5# Copyright (C) 2020-2020 Bareos GmbH & Co. KG
6#
7# This program is Free Software; you can redistribute it and/or
8# modify it under the terms of version three of the GNU Affero General Public
9# License as published by the Free Software Foundation, which is
10# listed in the file LICENSE.
11#
12# This program is distributed in the hope that it will be useful, but
13# WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15# Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
20# 02110-1301, USA.
21#
22# Author: Maik Aussendorf
23#
24
25from datetime import datetime, timedelta
26from typing import Optional, List
27from fastapi import Depends, FastAPI, HTTPException, status, Response, Path, Body, Query
28from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
29from jose import JWTError, jwt
30from passlib.context import CryptContext
31from pydantic import BaseModel, Field, PositiveInt
32from enum import Enum
33import pathlib
34from packaging import version
35import bareos.bsock
36import configparser
37import yaml
38from bareosRestapiModels import *
39
40# Read config from api.ini
41config = configparser.ConfigParser()
42config.read("api.ini")
43
44CONFIG_DIRECTOR_ADDRESS = config.get("Director", "Address")
45CONFIG_DIRECTOR_NAME = config.get("Director", "Name")
46CONFIG_DIRECTOR_PORT = config.getint("Director", "Port")
47
48SECRET_KEY = config.get("JWT", "secret_key")
49ALGORITHM = config.get("JWT", "algorithm")
50ACCESS_TOKEN_EXPIRE_MINUTES = config.getint("JWT", "access_token_expire_minutes")
51
52userDirectors = {}
53users_db = {}
54
55with open("metatags.yaml", "r") as stream:
56    tags_metadata = yaml.safe_load(stream)
57
58pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
59
60oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
61
62app = FastAPI(
63    title="Bareos REST API",
64    description="Bareos REST API built on python-bareos. Experimental and subject to enhancements and changes. **Note** swagger does not support GET methods with bodies, however, the CURL statements displayed by swagger do work.",
65    version="0.0.1",
66    openapi_tags=tags_metadata,
67)
68
69
70class UserObject(object):
71    def __init__(self, username, password):
72        # self.id = id
73        self.username = username
74        self.password = password
75        self.directorName = CONFIG_DIRECTOR_NAME
76        self.director = bareos.bsock.BSock
77        self.jsonDirector = bareos.bsock.DirectorConsoleJson
78        self.directorVersion = ""  # Format: xx.yy.zz, example: 19.02.06
79
80    def __str__(self):
81        return "User(username='%s')" % (self.username)
82
83    def __iter__(self):
84        yield "username", self.username
85        yield "directorName", self.directorName
86        yield "directorVersion", self.directorVersion
87
88    def getDirectorVersion(self):
89        return self.directorVersion
90
91
92def verify_password(plain_password, hashed_password):
93    return pwd_context.verify(plain_password, hashed_password)
94
95
96def get_password_hash(password):
97    return pwd_context.hash(password)
98
99
100def get_user(username: str):
101    if username in users_db:
102        # print(users_db[username])
103        # return {'username':username, 'directorName': users_db[username].directorName}
104        return users_db[username]
105
106
107def authenticate_user(username: str, password: str):
108    jsonDirector = None
109    try:
110        jsonDirector = bareos.bsock.DirectorConsoleJson(
111            address=CONFIG_DIRECTOR_ADDRESS,
112            port=CONFIG_DIRECTOR_PORT,
113            dirname=CONFIG_DIRECTOR_NAME,
114            name=username,
115            password=bareos.bsock.Password(password),
116        )
117    except Exception as e:
118        print(
119            "Could not authorize %s at director %s. %s"
120            % (username, CONFIG_DIRECTOR_NAME, e)
121        )
122        return False
123    user = UserObject(username, password)
124    user.jsonDirector = jsonDirector
125    user.username = username
126    users_db[username] = user
127    return user
128
129
130def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
131    to_encode = data.copy()
132    if expires_delta:
133        expire = datetime.utcnow() + expires_delta
134    else:
135        expire = datetime.utcnow() + timedelta(minutes=15)
136    to_encode.update({"exp": expire})
137    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
138    return encoded_jwt
139
140
141async def get_current_user(token: str = Depends(oauth2_scheme)):
142    credentials_exception = HTTPException(
143        status_code=status.HTTP_401_UNAUTHORIZED,
144        detail="Could not validate credentials",
145        headers={"WWW-Authenticate": "Bearer"},
146    )
147    try:
148        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
149        username: str = payload.get("sub")
150        if username is None:
151            raise credentials_exception
152        token_data = TokenData(username=username)
153    except JWTError:
154        raise credentials_exception
155    user = get_user(username=token_data.username)
156    if user is None:
157        raise credentials_exception
158    return user
159
160
161@app.post("/token", response_model=Token)
162async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
163    user = authenticate_user(form_data.username, form_data.password)
164    if not user:
165        raise HTTPException(
166            status_code=status.HTTP_401_UNAUTHORIZED,
167            detail="Incorrect username or password",
168            headers={"WWW-Authenticate": "Bearer"},
169        )
170    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
171    access_token = create_access_token(
172        data={"sub": user.username}, expires_delta=access_token_expires
173    )
174    return {"access_token": access_token, "token_type": "bearer"}
175
176
177@app.get("/users/me/", response_model=User)
178async def read_users_me(current_user: User = Depends(get_current_user)):
179    return current_user
180
181
182## Generic Methods
183
184
185def versionCheck(
186    *,
187    response: Response,
188    current_user: User = Depends(get_current_user),
189    minVersion: Optional[str] = "16.1.1",
190):
191    myVersion = ""
192    if current_user.directorVersion > "":
193        myVersion = current_user.directorVersion
194    else:
195        result = read_director_version(response=response, current_user=current_user)
196        if "version" in result:
197            myVersion = result["version"]
198            current_user.directorVersion = myVersion
199        else:
200            raise HTTPException(
201                status_code=500,
202                detail="Could not read version from director. Need at least version %s"
203                % (minVersion),
204            )
205    # print (myVersion)
206    if not (version.parse(myVersion) >= version.parse(minVersion)):
207        raise HTTPException(
208            status_code=501,
209            detail="Not implemented in Bareos %s. Need at least version %s"
210            % (myVersion, minVersion),
211        )
212    else:
213        return True
214
215
216def configure_add_standard_component(
217    *,
218    componentDef: BaseModel,
219    response: Response,
220    current_user: User = Depends(get_current_user),
221    componentType=str,
222):
223    """
224    Create a new Bareos standard component resource.
225    Console command used: _configure add component_
226
227    """
228
229    addCommand = "configure add %s" % componentType
230    componentDict = componentDef.dict()
231
232    for a in componentDict:
233        if componentDict[a] is not None:
234            addCommand += " %s=%s" % (
235                a,
236                str(componentDict[a]).strip("[]").replace("'", "").replace(" ", ""),
237            )
238    # print(addCommand)
239    # print(current_user)
240    try:
241        result = current_user.jsonDirector.call(addCommand)
242    except Exception as e:
243        response.status_code = 500
244        return {
245            "message": "Could not add %s with command '%s'. Message: '%s'"
246            % (componentType, addCommand, e)
247        }
248
249    if "configure" in result and "add" in result["configure"]:
250        return result
251    else:
252        response.status_code = 500
253        return {
254            "message": "Could not add %s with command '%s' on director %s. Message: '%s'"
255            % (componentType, addCommand, current_user.directorName, e)
256        }
257
258
259def switch_resource(
260    *,
261    response: Response,
262    current_user: User = Depends(get_current_user),
263    resourceName: str,
264    componentType=str,
265    enable=bool,
266):
267    """
268    Enables or disables a client, job or schedule
269    Returns tuple: success(bool), json-return-string(dict)
270    """
271    responseDict = {}
272    if enable:
273        command = "enable "
274    else:
275        command = "disable "
276    command += "%s=%s" % (componentType, resourceName)
277    # print(command)
278    try:
279        responseDict = current_user.jsonDirector.call(command)
280    except Exception as e:
281        response.status_code = 500
282        return (
283            False,
284            {
285                "message": "Could not en/disable %s %s on director %s. Message: '%s'"
286                % (componentType, resourceName, CONFIG_DIRECTOR_NAME, e)
287            },
288        )
289    # json result from director is empty
290    response.status_code = 204
291    return (True, responseDict)
292
293
294def parseCommandOptions(queryDict):
295    optionString = ""
296    for q in queryDict:
297        if queryDict[q] is not None:
298            if type(queryDict[q]) != bareosFlag:
299                optionString += " %s=%s" % (q, str(queryDict[q]))
300            else:
301                if queryDict[q]:
302                    optionString += " %s" % q
303    return optionString
304
305
306def show_configuration_items(
307    *,
308    response: Response,
309    current_user: User = Depends(get_current_user),
310    itemType: str,
311    byName: Optional[str] = None,
312    verbose: Optional[bareosBool] = "yes",
313):
314    """
315    Uses _show_ command to provide configuration setting
316    """
317    versionCheck(
318        response=response,
319        current_user=current_user,
320        minVersion="20.0.0~pre996.de46d0b15",
321    )
322    # Sometimes config type identificator differs from key returned by director, we need to map
323    itemKey = itemType
324    # itemTypeKeyMap = {"clients":"client", "jobs":"job", "pools":"pool", "schedules": "schedule", "storages":"storage", "users":"user", "profiles":"profile", "consoles":"console"}
325    # if itemType in itemTypeKeyMap:
326    #    itemKey = itemTypeKeyMap[itemType]
327
328    foundItems = 0
329    showCommand = "show %s" % itemType
330    if byName:
331        showCommand += "=%s" % byName
332    if verbose:
333        # print ("verbose on")
334        showCommand += " verbose"
335    try:
336        responseDict = current_user.jsonDirector.call(showCommand)
337    except Exception as e:
338        raise HTTPException(
339            status_code=500,
340            detail="Could not read %s from director %s. Message: '%s'"
341            % (itemType, CONFIG_DIRECTOR_NAME, e),
342        )
343    # print(responseDict)
344    if itemKey in responseDict:
345        foundItems = len(responseDict[itemKey])
346    if foundItems > 0 and byName:
347        return responseDict[itemKey]
348    elif foundItems > 0:
349        return {"totalItems": foundItems, itemType: responseDict[itemKey]}
350    else:
351        raise HTTPException(status_code=404, detail="No %s found." % itemKey)
352
353
354def list_catalog_items(
355    *,
356    response: Response,
357    current_user: User = Depends(get_current_user),
358    itemType: str,
359    limit: Optional[int] = None,
360    offset: Optional[int] = None,
361    jobQuery: Optional[jobQuery] = None,
362    verbose: Optional[bareosBool] = "yes",
363    hasCountOption: Optional[bareosBool] = "no",
364):
365    itemKey = itemType
366    itemTypeKeyMap = {"files": "filenames"}
367    if itemType in itemTypeKeyMap:
368        itemKey = itemTypeKeyMap[itemType]
369    if verbose:
370        listCommand = "llist "
371    else:
372        listCommand = "list "
373    listCommand += itemType
374    queryDict = {}
375    countDict = {}
376    results = {}
377    countCommand = listCommand
378    if jobQuery is not None:
379        queryDict = jobQuery.dict()
380    for q in queryDict:
381        if queryDict[q] is not None:
382            listCommand += " %s=%s" % (q, str(queryDict[q]))
383            countCommand += " %s=%s" % (q, str(queryDict[q]))
384    if limit is not None:
385        listCommand += " limit=%d" % limit
386    if offset is not None:
387        listCommand += " offset=%d" % offset
388    countCommand += " count"
389    try:
390        responseDict = current_user.jsonDirector.call(listCommand)
391        if hasCountOption:
392            countDict = current_user.jsonDirector.call(countCommand)
393    except Exception as e:
394        raise HTTPException(
395            status_code=500,
396            detail={
397                "message": "Could not read %s list from director %s. Message: '%s'"
398                % (itemType, CONFIG_DIRECTOR_NAME, e)
399            },
400        )
401    foundItems = len(responseDict)
402    if (
403        hasCountOption == "yes"
404        and itemKey in countDict
405        and "count" in countDict[itemKey][0]
406    ):
407        results["totalItems"] = countDict[itemKey][0]["count"]
408    else:
409        results["totalItems"] = foundItems
410    if foundItems > 0:
411        # check for limit / offset
412        if limit is not None:
413            results["limit"] = limit
414        if offset is not None:
415            results["offset"] = offset
416        return {**results, itemKey: responseDict[itemKey]}
417    else:
418        raise HTTPException(
419            status_code=404, detail={"message": "No %s found." % itemType}
420        )
421
422
423### Clients ###
424@app.get("/control/clients", status_code=200, tags=["clients", "control"])
425def read_catalog_info_for_all_clients(
426    response: Response,
427    current_user: User = Depends(get_current_user),
428    name: Optional[str] = None,
429):
430    """
431    Read status information from catalog about all clients or just one client by name.
432    Built on console command _llist client_
433    """
434    if name:
435        listCommand = "llist client=%s" % name
436    else:
437        listCommand = "llist clients"
438    try:
439        responseDict = current_user.jsonDirector.call(listCommand)
440    except Exception as e:
441        response.status_code = 500
442        return {
443            "message": "Could not read client list from director %s. Message: '%s'"
444            % (CONFIG_DIRECTOR_NAME, e)
445        }
446    if "clients" in responseDict:
447        totalItems = len(responseDict["clients"])
448        return {"totalItems": totalItems, "clients": responseDict["clients"]}
449    else:
450        response.status_code = 404
451        return {"message": "No clients found."}
452
453
454@app.get("/control/clients/{client_id}", tags=["clients", "control"])
455def read_catalog_info_for_particular_client(
456    *,
457    client_id: int = Path(..., title="The ID of client to get", ge=1),
458    response: Response,
459    current_user: User = Depends(get_current_user),
460):
461    """
462    Read catalog information for one client by id.
463    Built on console command _llist client_
464
465    **Warning** Director does not support direct query by _id_ we query all clients and filter the result.
466    Maybe more time consuming than expected in large settings.
467    """
468    allClients = read_catalog_info_for_all_clients(response, current_user)
469    result = None
470    for c in allClients["clients"]:
471        if c["clientid"] == str(client_id):
472            result = c
473            break
474    if result:
475        return result
476    else:
477        response.status_code = 404
478        return {
479            "message": "Client with Client ID {client_id} not found".format(
480                client_id=client_id
481            )
482        }
483    return {"item_id": item_id}
484
485
486@app.put(
487    "/control/clients/enable/{client_name}",
488    status_code=204,
489    tags=["clients", "control"],
490)
491def enable_client(
492    *,
493    client_name: str = Path(..., title="The client (name) to enable"),
494    response: Response,
495    current_user: User = Depends(get_current_user),
496):
497    (result, jsonMessage) = switch_resource(
498        response=response,
499        current_user=current_user,
500        resourceName=client_name,
501        componentType="client",
502        enable=True,
503    )
504    if result:
505        response.status_code = 204  # ok, but empty return-string
506    else:
507        response.status_code = 500
508    return jsonMessage
509
510
511@app.put(
512    "/control/clients/disable/{client_name}",
513    status_code=204,
514    tags=["clients", "control"],
515)
516def disable_client(
517    *,
518    client_name: str = Path(..., title="The client (name) to disable"),
519    response: Response,
520    current_user: User = Depends(get_current_user),
521):
522    (result, jsonMessage) = switch_resource(
523        response=response,
524        current_user=current_user,
525        resourceName=client_name,
526        componentType="client",
527        enable=False,
528    )
529    if result:
530        response.status_code = 204  # ok, but empty return-string
531    else:
532        response.status_code = 500
533    return jsonMessage
534
535
536@app.get("/configuration/clients", tags=["clients", "configuration"])
537def read_all_clients(
538    *,
539    response: Response,
540    current_user: User = Depends(get_current_user),
541    verbose: Optional[bareosBool] = Query("yes", title="Verbose output"),
542):
543    """
544    Read all jobdef resources. Built on console command _show clients_.
545
546    Needs at least Bareos Version >= 20.0.0
547    """
548    return show_configuration_items(
549        response=response,
550        current_user=current_user,
551        itemType="clients",
552        verbose=verbose,
553    )
554
555
556@app.get("/configuration/clients/{clients_name}", tags=["clients", "configuration"])
557def read_client_by_name(
558    *,
559    response: Response,
560    current_user: User = Depends(get_current_user),
561    clients_name: str = Path(..., title="Client name to look for"),
562    verbose: Optional[bareosBool] = Query("yes", title="Verbose output"),
563):
564    """
565    Read all jobdef resources. Built on console command _show clients_.
566
567    Needs at least Bareos Version >= 20.0.0
568    """
569    return show_configuration_items(
570        response=response,
571        current_user=current_user,
572        itemType="clients",
573        byName=clients_name,
574        verbose=verbose,
575    )
576
577
578@app.post("/configuration/clients", tags=["clients", "configuration"])
579def post_client(
580    *,
581    clientDef: clientResource = Body(..., title="The client to create"),
582    response: Response,
583    current_user: User = Depends(get_current_user),
584):
585    return configure_add_standard_component(
586        componentDef=clientDef,
587        response=response,
588        current_user=current_user,
589        componentType="client",
590    )
591
592
593### filesets
594
595
596@app.get("/configuration/filesets", tags=["filesets", "configuration"])
597def read_all_filesets(
598    *,
599    response: Response,
600    current_user: User = Depends(get_current_user),
601    verbose: Optional[bareosBool] = Query("yes", title="Verbose output"),
602):
603    """
604    Read all jobdef resources. Built on console command _show filesets_.
605
606    Needs at least Bareos Version >= 20.0.0
607    """
608    return show_configuration_items(
609        response=response,
610        current_user=current_user,
611        itemType="filesets",
612        verbose=verbose,
613    )
614
615
616@app.get("/configuration/filesets/{filesets_name}", tags=["filesets", "configuration"])
617def read_fileset_by_name(
618    *,
619    response: Response,
620    current_user: User = Depends(get_current_user),
621    filesets_name: str = Path(..., title="fileset name to look for"),
622    verbose: Optional[bareosBool] = Query("yes", title="Verbose output"),
623):
624    """
625    Read all jobdef resources. Built on console command _show filesets_.
626
627    Needs at least Bareos Version >= 20.0.0
628    """
629    return show_configuration_items(
630        response=response,
631        current_user=current_user,
632        itemType="filesets",
633        byName=filesets_name,
634        verbose=verbose,
635    )
636
637
638#### Job Control
639
640
641@app.post("/control/jobs/run", tags=["jobcontrol", "control", "jobs"])
642def runJob(
643    *,
644    jobControl: jobControl = Body(..., title="Job control information", embed=True),
645    response: Response,
646    current_user: User = Depends(get_current_user),
647):
648    """
649    Run a job, defined by jobControl record.
650
651    **Note**: Swagger throws a weird error when running this command by the UI,
652    while the given curl statement works fine.
653    """
654    result = None
655    jobCommand = "run"
656    args = jobControl.dict()
657    for a in args:
658        if args[a] is not None:
659            jobCommand += " %s=%s" % (a, str(args[a]))
660    # print(jobCommand)
661    try:
662        result = current_user.jsonDirector.call(jobCommand)
663    except Exception as e:
664        response.status_code = 500
665        return {
666            "message": "Could not start job '%s' on director %s. Message: '%s'"
667            % (jobCommand, current_user.directorName, e)
668        }
669    if "run" in result and "jobid" in result["run"]:
670        return {"jobid": int(result["run"]["jobid"])}
671    else:
672        response.status_code = 500
673        return {"message": "Job '%s' triggered but no jobId returned" % jobCommand}
674
675
676@app.post("/control/jobs/rerun/{job_id}", tags=["jobcontrol", "control", "jobs"])
677def rerun_Job_by_jobid(
678    *,
679    job_id: int = Path(..., title="The ID of job to rerun", ge=1),
680    response: Response,
681    current_user: User = Depends(get_current_user),
682):
683    """
684    Rerun a specific job given bei jobid
685    """
686    result = None
687    rerunCommand = "rerun jobid=%d" % job_id
688    try:
689        result = current_user.jsonDirector.call(rerunCommand)
690    except Exception as e:
691        response.status_code = 500
692        return {
693            "message": "Could not rerun jobid %d on director %s. Message: '%s'"
694            % (job_id, current_user.directorName, e)
695        }
696    if "run" in result and "jobid" in result["run"]:
697        return {"jobid": int(result["run"]["jobid"])}
698    else:
699        response.status_code = 500
700        return {"message": "Job '%s' triggered but no jobId returned" % jobCommand}
701
702
703@app.post("/control/jobs/rerun", tags=["jobcontrol", "control", "jobs"])
704def rerun_Job(
705    *,
706    job_range: jobRange = Body(..., title="Job range to rerun"),
707    response: Response,
708    current_user: User = Depends(get_current_user),
709):
710    """
711    Rerun jobs given by the following parameters (at least one)
712
713    - **since_jobid**=_jobid_ rerun failed jobs since this jobid
714    - **until_jobid**=_jobid_ - in conjunction with _since_jobid_
715    - **days**=_nr_days_ - since a number of days
716    - **hours**=_nr_hours_ - since a number of hours
717
718    Built on console command _rerun_
719    """
720    result = None
721    rerunCommand = "rerun"
722    args = job_range.dict()
723    for a in args:
724        if args[a] is not None:
725            rerunCommand += " %s=%s" % (a, args[a])
726    rerunCommand += " yes"
727    try:
728        result = current_user.jsonDirector.call(rerunCommand)
729    except Exception as e:
730        response.status_code = 500
731        return {
732            "message": "Could not %s on director %s. Message: '%s'"
733            % (rerunCommand, current_user.directorName, e)
734        }
735    # print(result)
736    if "run" in result and "jobid" in result["run"]:
737        return {"jobid": int(result["run"]["jobid"])}
738    else:
739        response.status_code = 500
740        return {"message": "Job '%s' triggered but no jobId returned" % rerunCommand}
741
742
743@app.post("/control/jobs/restore", tags=["jobcontrol", "control", "jobs"])
744def runRestoreJob(
745    *,
746    jobControl: restoreJobControl = Body(
747        ..., title="Restore Job control information", embed=True
748    ),
749    response: Response,
750    current_user: User = Depends(get_current_user),
751):
752    """
753    Run a restore-job, defined by jobControl record.
754
755    """
756    result = None
757    jobCommand = "restore"
758    args = jobControl.dict()
759    for a in args:
760        # print(" %s=%s" % (a, str(args[a])))
761        if args[a] is not None:
762            if a != "selectAllDone":
763                jobCommand += " %s=%s" % (a, str(args[a]))
764            elif args[a] == "yes":
765                jobCommand += " select all done"
766    # print(jobCommand)
767    try:
768        result = current_user.jsonDirector.call(jobCommand)
769    except Exception as e:
770        response.status_code = 500
771        return {
772            "message": "Could not start job '%s' on director %s. Message: '%s'"
773            % (jobCommand, current_user.directorName, e)
774        }
775    if "run" in result and "jobid" in result["run"]:
776        return {"jobid": int(result["run"]["jobid"])}
777    else:
778        response.status_code = 500
779        return {"message": "Job '%s' triggered but no jobId returned" % jobCommand}
780
781
782@app.put("/control/jobs/cancel/{job_id}", tags=["jobcontrol", "control", "jobs"])
783def cancelJob(
784    *,
785    job_id: int = Path(..., title="The ID of job to cancel", ge=1),
786    response: Response,
787    current_user: User = Depends(get_current_user),
788):
789    """
790    Cancel a specific job given bei jobid
791    """
792    # cancel a specific job given bei jobid
793    cancelCommand = "cancel jobid=%d" % job_id
794    result = None
795    try:
796        result = current_user.jsonDirector.call(cancelCommand)
797    except Exception as e:
798        response.status_code = 500
799        return {
800            "message": "Could not cancel jobid %d on director %s. Message: '%s'"
801            % (job_id, current_user.directorName, e)
802        }
803    return result
804
805
806@app.put(
807    "/control/jobs/enable/{job_name}",
808    status_code=204,
809    tags=["jobcontrol", "jobs", "control"],
810)
811def enable_job(
812    *,
813    job_name: str = Path(..., title="The job (name) to enable"),
814    response: Response,
815    current_user: User = Depends(get_current_user),
816):
817    (result, jsonMessage) = switch_resource(
818        response=response,
819        current_user=current_user,
820        resourceName=job_name,
821        componentType="job",
822        enable=True,
823    )
824    if result:
825        response.status_code = 204  # ok, but empty return-string
826    else:
827        response.status_code = 500
828    return jsonMessage
829
830
831@app.put(
832    "/control/jobs/disable/{job_name}",
833    status_code=204,
834    tags=["jobcontrol", "jobs", "control"],
835)
836def disable_job(
837    *,
838    job_name: str = Path(..., title="The job (name) to disable"),
839    response: Response,
840    current_user: User = Depends(get_current_user),
841):
842    (result, jsonMessage) = switch_resource(
843        response=response,
844        current_user=current_user,
845        resourceName=job_name,
846        componentType="job",
847        enable=False,
848    )
849    if result:
850        response.status_code = 204  # ok, but empty return-string
851    else:
852        response.status_code = 500
853    return jsonMessage
854
855
856#### Job Status
857
858
859@app.get("/control/jobs/totals", tags=["jobcontrol", "control", "jobs"])
860def read_all_jobs_totals(
861    *, response: Response, current_user: User = Depends(get_current_user)
862):
863    listCommand = "llist jobtotals"
864    results = {}
865    try:
866        responseDict = current_user.jsonDirector.call(listCommand)
867    except Exception as e:
868        response.status_code = 500
869        return {
870            "message": "Could not read job totals from director %s. Message: '%s'"
871            % (CONFIG_DIRECTOR_NAME, e)
872        }
873    if "jobtotals" in responseDict:
874        return responseDict
875    else:
876        response.status_code = 404
877        return {"message": "No jobtotals found."}
878
879
880@app.get("/control/jobs/{job_id}", tags=["jobcontrol", "control", "jobs"])
881def read_job_status(
882    *,
883    job_id: int = Path(..., title="The ID of job to get", ge=1),
884    response: Response,
885    current_user: User = Depends(get_current_user),
886):
887    """
888    Read information about a specific job defined by jobid
889    Returns output of command _llist jobid=id_
890    """
891    result = None
892    listCommand = "llist jobid=%d" % job_id
893    try:
894        result = current_user.jsonDirector.call(listCommand)
895    except Exception as e:
896        response.status_code = 500
897        return {
898            "message": "Could not query jobs on director %s. Message: '%s'"
899            % (current_user.directorName, e)
900        }
901    if result and "jobs" in result:
902        return result["jobs"][0]
903    else:
904        response.status_code = 404
905        return {"message": "Job with Job ID {jobid} not found".format(jobid=job_id)}
906
907
908@app.get("/control/jobs", tags=["jobcontrol", "control", "jobs"])
909def read_all_jobs_status(
910    *,
911    response: Response,
912    current_user: User = Depends(get_current_user),
913    limit: Optional[int] = Query(None, title="Result items limit", gt=1),
914    offset: Optional[int] = Query(None, title="Result items offset", gt=0),
915    jobQuery: Optional[jobQuery] = Body(None, title="Query parameter"),
916):
917    return list_catalog_items(
918        itemType="jobs",
919        current_user=current_user,
920        response=response,
921        limit=limit,
922        offset=offset,
923        jobQuery=jobQuery,
924        hasCountOption="yes",
925    )
926
927
928@app.delete("/control/jobs/{job_id}", tags=["jobcontrol", "control", "jobs"])
929def delete_job(
930    *,
931    job_id: int = Path(..., title="The ID of job to delete", ge=1),
932    response: Response,
933    current_user: User = Depends(get_current_user),
934):
935    """
936    Delete job record from catalog
937    """
938    # Director gives no success nor failed information
939    # We implemente validation here (check if job exists before and after deletion)
940    jobStatusResponse = read_job_status(
941        job_id=job_id, response=response, current_user=current_user
942    )
943    if not "jobid" in jobStatusResponse:
944        response.status_code = 404
945        return {
946            "message": "No job with id %d found on director %s."
947            % (job_id, current_user.directorName)
948        }
949    # delete a specific job record given bei jobid
950    deleteCommand = "delete jobid=%d" % job_id
951    try:
952        result = current_user.jsonDirector.call(deleteCommand)
953    except Exception as e:
954        response.status_code = 500
955        return {
956            "message": "Could not delete jobid %d on director %s. Message: '%s'"
957            % (job_id, current_identity.directorName, e)
958        }
959    jobStatusResponse = read_job_status(
960        job_id=job_id, response=response, current_user=current_user
961    )
962    if "jobid" in jobStatusResponse:
963        response.status_code = 500
964        return {
965            "message": "Job with id %d still exists on director %s. Delete failed"
966            % (job_id, current_user.directorName)
967        }
968    response.status_code = 200
969    return {"message": "Job %d succesfully deleted." % job_id}
970
971
972@app.get("/control/jobs/logs/{job_id}", tags=["jobcontrol", "control", "jobs"])
973def read_one_job_log(
974    *,
975    job_id: int = Path(..., title="The ID of job to get the logs", ge=1),
976    response: Response,
977    current_user: User = Depends(get_current_user),
978):
979    """
980    Read logs from a specific job defined by jobid
981    Returns output of command _list joblog jobid=id_
982    """
983    result = None
984    listCommand = "list joblog jobid=%d" % job_id
985    try:
986        result = current_user.jsonDirector.call(listCommand)
987    except Exception as e:
988        response.status_code = 500
989        return {
990            "message": "Could not read joblogs on director %s. Message: '%s'"
991            % (current_user.directorName, e)
992        }
993    if result and "joblog" in result:
994        totalItems = len(result["joblog"])
995        return {"totalItems": totalItems, "joblog": result["joblog"]}
996    else:
997        response.status_code = 404
998        return {"message": "Joblogs Job with ID {jobid} not found".format(jobid=job_id)}
999
1000
1001@app.get("/control/jobs/files/{job_id}", tags=["jobcontrol", "control", "jobs"])
1002def read_files_of_job(
1003    *,
1004    job_id: int = Path(..., title="The ID of job to get the files", ge=1),
1005    response: Response,
1006    current_user: User = Depends(get_current_user),
1007):
1008    """
1009    Read files from a specific job defined by jobid
1010    Returns output of command _list joblog jobid=id_
1011    """
1012    result = None
1013    listCommand = "list files jobid=%d" % job_id
1014    try:
1015        result = current_user.jsonDirector.call(listCommand)
1016    except Exception as e:
1017        response.status_code = 500
1018        return {
1019            "message": "Could not read jobfiles on director %s. Message: '%s'"
1020            % (current_user.directorName, e)
1021        }
1022    if result and "filenames" in result:
1023        totalItems = len(result["filenames"])
1024        return {"totalItems": totalItems, "filenames": result["filenames"]}
1025    else:
1026        response.status_code = 404
1027        return {
1028            "message": "Files for job with ID {jobid} not found".format(jobid=job_id)
1029        }
1030
1031
1032#### JobDefs
1033
1034
1035@app.post("/confguration/jobdefs", tags=["jobdefs", "configuration"])
1036def post_jobdef(
1037    *,
1038    jobDef: jobDefs = Body(..., title="Jobdef resource"),
1039    response: Response,
1040    current_user: User = Depends(get_current_user),
1041):
1042    """
1043    Create a new jobdefs resource.
1044    Console command used: _configure add jobdefs_
1045    """
1046    return configure_add_standard_component(
1047        componentDef=jobDef,
1048        response=response,
1049        current_user=current_user,
1050        componentType="jobdefs",
1051    )
1052
1053
1054@app.get("/configuration/jobdefs", tags=["jobdefs", "configuration"])
1055def read_all_jobdefs(
1056    *,
1057    response: Response,
1058    current_user: User = Depends(get_current_user),
1059    verbose: Optional[bareosBool] = Query("yes", title="Verbose output"),
1060):
1061    """
1062    Read all jobdef resources. Built on console command _show jobdefs_.
1063
1064    Needs at least Bareos Version >= 20.0.0
1065    """
1066    return show_configuration_items(
1067        response=response,
1068        current_user=current_user,
1069        itemType="jobdefs",
1070        verbose=verbose,
1071    )
1072
1073
1074@app.get("/configuration/jobdefs/{jobdefs_name}", tags=["jobdefs", "configuration"])
1075def read_jobdef_by_name(
1076    *,
1077    response: Response,
1078    current_user: User = Depends(get_current_user),
1079    jobdefs_name: str = Path(..., title="JobDef name to look for"),
1080    verbose: Optional[bareosBool] = Query("yes", title="Verbose output"),
1081):
1082    """
1083    Read all jobdef resources. Built on console command _show jobdefs_.
1084
1085    Needs at least Bareos Version >= 20.0.0
1086    """
1087    return show_configuration_items(
1088        response=response,
1089        current_user=current_user,
1090        itemType="jobdefs",
1091        byName=jobdefs_name,
1092        verbose=verbose,
1093    )
1094
1095
1096#### Job Resource
1097
1098
1099@app.get("/configuration/jobs", tags=["jobs", "configuration"])
1100def read_all_jobs(
1101    *,
1102    response: Response,
1103    current_user: User = Depends(get_current_user),
1104    verbose: Optional[bareosBool] = Query("yes", title="Verbose output"),
1105):
1106    """
1107    Read all jobdef resources. Built on console command _show jobs_.
1108
1109    Needs at least Bareos Version >= 20.0.0
1110    """
1111    return show_configuration_items(
1112        response=response, current_user=current_user, itemType="jobs", verbose=verbose
1113    )
1114
1115
1116@app.get("/configuration/jobs/{jobs_name}", tags=["jobs", "configuration"])
1117def read_job_by_name(
1118    *,
1119    response: Response,
1120    current_user: User = Depends(get_current_user),
1121    jobs_name: str = Path(..., title="Client name to look for"),
1122    verbose: Optional[bareosBool] = Query("yes", title="Verbose output"),
1123):
1124    """
1125    Read all jobdef resources. Built on console command _show jobs_.
1126
1127    Needs at least Bareos Version >= 20.0.0
1128    """
1129    return show_configuration_items(
1130        response=response,
1131        current_user=current_user,
1132        itemType="jobs",
1133        byName=jobs_name,
1134        verbose=verbose,
1135    )
1136
1137
1138@app.post("/configuration/jobs", tags=["jobs", "configuration"])
1139def post_job(
1140    *,
1141    jobDef: jobResource = Body(..., title="Job resource"),
1142    response: Response,
1143    current_user: User = Depends(get_current_user),
1144):
1145    """
1146    Create a new job resource.
1147    Console command used: _configure add job_
1148
1149    """
1150    return configure_add_standard_component(
1151        componentDef=jobDef,
1152        response=response,
1153        current_user=current_user,
1154        componentType="job",
1155    )
1156
1157
1158### Volumes
1159
1160
1161@app.get("/control/volumes", tags=["volumes", "control"])
1162def read_volumes(
1163    *,
1164    response: Response,
1165    current_user: User = Depends(get_current_user),
1166    limit: Optional[int] = Query(None, title="Result items limit", gt=0),
1167    offset: Optional[int] = Query(None, title="Result items offset", gt=0),
1168    myQuery: Optional[volumeQuery] = Body(None, title="Query parameter"),
1169):
1170    queryDict = {}
1171    countDict = {}
1172    responseDict = {}
1173    results = {}
1174    results["volumes"] = {}
1175    volumeKeyName = "volumes"
1176    volumeCommand = "llist volumes"
1177    countCommand = "list volumes"
1178    if myQuery is not None:
1179        queryDict = myQuery.dict()
1180    for q in queryDict:
1181        if queryDict[q] is not None:
1182            volumeCommand += " %s=%s" % (q, str(queryDict[q]))
1183            countCommand += " %s=%s" % (q, str(queryDict[q]))
1184    countCommand += " count"
1185    if limit is not None:
1186        volumeCommand += " limit=%d" % limit
1187    if offset is not None:
1188        volumeCommand += " offset=%d" % offset
1189    # if volume name is in queryDict, we have to remove the other filters
1190    # and use a different comman
1191    if "volume" in queryDict and queryDict["volume"] is not None:
1192        volumeKeyName = "volume"
1193        volumeCommand = "llist volume=%s" % queryDict["volume"]
1194        countCommand = None
1195    try:
1196        responseDict = current_user.jsonDirector.call(volumeCommand)
1197        if countCommand is not None:
1198            countDict = current_user.jsonDirector.call(countCommand)
1199    except Exception as e:
1200        response.status_code = 500
1201        return {
1202            "message": "Could not read volume list from director %s. Message: '%s'"
1203            % (CONFIG_DIRECTOR_NAME, e)
1204        }
1205    if "volumes" in responseDict:
1206        counter = 0
1207        # countDict/response dict has different structures, if filtered by pool or not
1208        if isinstance(countDict["volumes"], dict):
1209            results["volumes"] = responseDict[volumeKeyName]
1210            for p in countDict["volumes"]:
1211                counter += int(countDict["volumes"][p][0]["count"])
1212        else:
1213            # check for empty pool
1214            if len(responseDict[volumeKeyName]) == 0:
1215                response.status_code = 404
1216                return {"message": "Nothing found. Command: %s" % volumeCommand}
1217            poolName = responseDict[volumeKeyName][0]["pool"]
1218            # print(responseDict[volumeKeyName])
1219            results["volumes"][poolName] = responseDict[volumeKeyName]
1220            counter = int(countDict["volumes"][0]["count"])
1221        results["totalItems"] = counter
1222        foundItems = len(responseDict)
1223    elif "volume" in responseDict and "pool" in responseDict["volume"]:
1224        foundItems = 1
1225        poolName = responseDict["volume"]["pool"]
1226        results["totalItems"] = 1
1227        results["volumes"] = {poolName: [responseDict["volume"]]}
1228    else:
1229        response.status_code = 404
1230        return {"message": "nothing found for command: %s" % volumeCommand}
1231
1232    if foundItems > 0:
1233        # check for limit / offset
1234        if limit is not None:
1235            results["limit"] = limit
1236        if offset is not None:
1237            results["offset"] = offset
1238        return results
1239    else:
1240        response.status_code = 404
1241        return {"message": "No volumes found."}
1242
1243
1244@app.get("/control/volumes/{volume_id}", tags=["volumes", "control"])
1245def read_volume(
1246    *,
1247    response: Response,
1248    current_user: User = Depends(get_current_user),
1249    volume_id: int = Path(..., title="Volume ID to look for", gt=0, example=1),
1250    volumeQuery: Optional[volumeQuery] = Body(None, title="Query parameter"),
1251):
1252    volumeCommand = "llist volumes"
1253    try:
1254        responseDict = current_user.jsonDirector.call(volumeCommand)
1255    except Exception as e:
1256        response.status_code = 500
1257        return {
1258            "message": "Could not read volume list from director %s. Message: '%s'"
1259            % (CONFIG_DIRECTOR_NAME, e)
1260        }
1261    if "volumes" in responseDict:
1262        for p in responseDict["volumes"]:
1263            for v in responseDict["volumes"][p]:
1264                if v["mediaid"] == str(volume_id):
1265                    response.status_code = 200
1266                    return v
1267    response.status_code = 404
1268    return {"message": "No volume with id %d found" % volume_id}
1269
1270
1271@app.post("/control/volumes", status_code=204, tags=["volumes", "control"])
1272def label_volume(
1273    *,
1274    response: Response,
1275    current_user: User = Depends(get_current_user),
1276    volumeLabel: volumeLabelDef = Body(..., title="Volume label properties"),
1277):
1278    """
1279    Label a new volume using the _label_" command
1280    """
1281    responseDict = {}
1282    labelCommand = "label"
1283    labelCommand += parseCommandOptions(volumeLabel.dict())
1284    try:
1285        responseDict = current_user.jsonDirector.call(labelCommand)
1286    except Exception as e:
1287        response.status_code = 500
1288        return {
1289            "message": "Could not label volume on director %s. Message: '%s'"
1290            % (CONFIG_DIRECTOR_NAME, e)
1291        }
1292    return responseDict
1293
1294
1295@app.patch("/control/volumes/{volume_name}", tags=["volumes", "control"])
1296def update_volume(
1297    *,
1298    response: Response,
1299    current_user: User = Depends(get_current_user),
1300    volume_name: str = Path(..., title="Volume Name to update", example="Full-1742"),
1301    volumeProps: volumeProperties = Body(..., title="Volume properties"),
1302):
1303    """
1304    Update a volume
1305    TODO: verify, that parameter are quoted correct
1306    """
1307    responseDict = {}
1308    updateCommand = "update volume=%s" % volume_name
1309    updateCommand += parseCommandOptions(volumeProps.dict())
1310    # print(updateCommand)
1311    try:
1312        responseDict = current_user.jsonDirector.call(updateCommand)
1313    except Exception as e:
1314        response.status_code = 500
1315        return {
1316            "message": "Could not update volume on director %s. Message: '%s'"
1317            % (CONFIG_DIRECTOR_NAME, e)
1318        }
1319    # Director delivers empty response, we want to return the changed volume's properties
1320    volQuery = volumeQuery()
1321    volQuery.volume = volume_name
1322    responseDict = read_volumes(
1323        response=response,
1324        current_user=current_user,
1325        myQuery=volQuery,
1326        limit=None,
1327        offset=None,
1328    )
1329    # TODO: responseDict is structured: {volumes:{poolname:[{volume}]}} - we just want to return the volume without list and pool dict around
1330    return responseDict
1331
1332
1333@app.put("/control/volumes/move", status_code=200, tags=["volumes", "control"])
1334def move_volume(
1335    *,
1336    response: Response,
1337    current_user: User = Depends(get_current_user),
1338    moveParams: volumeMove = Body(..., title="Volume move parameters"),
1339):
1340    """
1341    Move a volume, using the _move_ command
1342    TODO: handle encrypt flag
1343    """
1344    responseDict = {}
1345    updateCommand = "move"
1346    updateCommand += parseCommandOptions(moveParams.dict())
1347    # print (updateCommand)
1348    try:
1349        responseDict = current_user.jsonDirector.call(updateCommand)
1350    except Exception as e:
1351        response.status_code = 500
1352        return {
1353            "message": "Could not move volumes on director %s. Message: '%s'"
1354            % (CONFIG_DIRECTOR_NAME, e)
1355        }
1356    # Director delivers empty response
1357    return responseDict
1358
1359
1360@app.put("/control/volumes/export", status_code=200, tags=["volumes", "control"])
1361def export_volume(
1362    *,
1363    response: Response,
1364    current_user: User = Depends(get_current_user),
1365    exportParams: volumeExport = Body(..., title="Volume Export parameters"),
1366):
1367    """
1368    Export volumes the _export_ command
1369    """
1370    responseDict = {}
1371    updateCommand = "export"
1372    updateCommand += parseCommandOptions(exportParams.dict())
1373    # print(updateCommand)
1374    try:
1375        responseDict = current_user.jsonDirector.call(updateCommand)
1376    except Exception as e:
1377        response.status_code = 500
1378        return {
1379            "message": "Could not export volumes on director %s. Message: '%s'"
1380            % (CONFIG_DIRECTOR_NAME, e)
1381        }
1382    # Director delivers empty response
1383    response.status_code = 200
1384    return responseDict
1385
1386
1387@app.put("/control/volumes/import", status_code=200, tags=["volumes", "control"])
1388def import_volume(
1389    *,
1390    response: Response,
1391    current_user: User = Depends(get_current_user),
1392    importParams: volumeImport = Body(..., title="Volume import parameters"),
1393):
1394    """
1395    import volumes the _import_ command
1396    """
1397    responseDict = {}
1398    updateCommand = "import"
1399    updateCommand += parseCommandOptions(importParams.dict())
1400    # print(updateCommand)
1401    try:
1402        responseDict = current_user.jsonDirector.call(updateCommand)
1403    except Exception as e:
1404        response.status_code = 500
1405        return {
1406            "message": "Could not import volumes on director %s. Message: '%s'"
1407            % (CONFIG_DIRECTOR_NAME, e)
1408        }
1409    # Director delivers empty response
1410    response.status_code = 200
1411    return responseDict
1412
1413
1414@app.put("/control/volumes/{volume_name}", status_code=204, tags=["volumes", "control"])
1415def relabel_volume(
1416    *,
1417    response: Response,
1418    current_user: User = Depends(get_current_user),
1419    volume_name: str = Path(
1420        ..., title="Old Volume Name to relabel", example="Full-1742"
1421    ),
1422    volumeRelabel: volumeRelabelDef = Body(..., title="New label properties"),
1423):
1424    """
1425    Relabel a volume, using the _relabel_ command
1426    TODO: handle encrypt flag
1427    """
1428    responseDict = {}
1429    updateCommand = "relabel oldvolume=%s" % volume_name
1430    updateCommand += parseCommandOptions(volumeRelabel.dict())
1431    # print(updateCommand)
1432    try:
1433        responseDict = current_user.jsonDirector.call(updateCommand)
1434    except Exception as e:
1435        response.status_code = 500
1436        return {
1437            "message": "Could not relabel volume on director %s. Message: '%s'"
1438            % (CONFIG_DIRECTOR_NAME, e)
1439        }
1440    # Director delivers empty response
1441    response.status_code = 204
1442    return responseDict
1443
1444
1445@app.delete(
1446    "/control/volumes/{volume_name}", status_code=204, tags=["volumes", "control"]
1447)
1448def delete_volume(
1449    *,
1450    response: Response,
1451    current_user: User = Depends(get_current_user),
1452    volume_name: str = Path(..., title="Volume Name to delete", example="Full-1742"),
1453):
1454    """
1455    Delete a volume from catalog using the _delete volume_ command.
1456    """
1457    responseDict = {}
1458    deleteCommand = "delete volume=%s yes" % volume_name
1459    try:
1460        responseDict = current_user.jsonDirector.call(deleteCommand)
1461    except Exception as e:
1462        response.status_code = 500
1463        return {
1464            "message": "Could not delete volume on director %s. Message: '%s'"
1465            % (CONFIG_DIRECTOR_NAME, e)
1466        }
1467    # Director delivers empty response
1468    response.status_code = 204
1469    return responseDict
1470
1471
1472### Pools
1473
1474
1475@app.get("/configuration/pools", tags=["pools", "configuration"])
1476def read_all_pools(
1477    *,
1478    response: Response,
1479    current_user: User = Depends(get_current_user),
1480    verbose: Optional[bareosBool] = Query("yes", title="Verbose output"),
1481):
1482    """
1483    Read all jobdef resources. Built on console command _show pools_.
1484
1485    Needs at least Bareos Version >= 20.0.0
1486    """
1487    return show_configuration_items(
1488        response=response, current_user=current_user, itemType="pools", verbose=verbose
1489    )
1490
1491
1492@app.get("/configuration/pools/{pools_name}", tags=["pools", "configuration"])
1493def read_pool_by_name(
1494    *,
1495    response: Response,
1496    current_user: User = Depends(get_current_user),
1497    pools_name: str = Path(..., title="Client name to look for"),
1498    verbose: Optional[bareosBool] = Query("yes", title="Verbose output"),
1499):
1500    """
1501    Read all jobdef resources. Built on console command _show pools_.
1502
1503    Needs at least Bareos Version >= 20.0.0
1504    """
1505    return show_configuration_items(
1506        response=response,
1507        current_user=current_user,
1508        itemType="pools",
1509        byName=pools_name,
1510        verbose=verbose,
1511    )
1512
1513
1514@app.post("/configuration/pools", tags=["pools", "configuration"])
1515def post_pool(
1516    *,
1517    poolDef: poolResource = Body(..., title="pool resource"),
1518    response: Response,
1519    current_user: User = Depends(get_current_user),
1520):
1521    """
1522    Create a new pool resource.
1523    Console command used: _configure add pool_
1524    """
1525    return configure_add_standard_component(
1526        componentDef=poolDef,
1527        response=response,
1528        current_user=current_user,
1529        componentType="pool",
1530    )
1531
1532
1533@app.get("/control/pools", status_code=200, tags=["pools", "control"])
1534def read_all_pools(
1535    response: Response,
1536    current_user: User = Depends(get_current_user),
1537    name: Optional[str] = None,
1538):
1539    """
1540    Read settings for all pools or just one pool by name from catalog.
1541    Built on console command _llist pool_
1542    """
1543    listCommand = ""
1544    if name:
1545        listCommand = "llist pool=%s" % name
1546    else:
1547        listCommand = "llist pools"
1548    # print(listCommand)
1549    try:
1550        responseDict = current_user.jsonDirector.call(listCommand)
1551    except Exception as e:
1552        response.status_code = 500
1553        return {
1554            "message": "Could not read pool list from director %s. Message: '%s'"
1555            % (CONFIG_DIRECTOR_NAME, e)
1556        }
1557    if "pools" in responseDict:
1558        totalItems = len(responseDict["pools"])
1559        return {"totalItems": totalItems, "pools": responseDict["pools"]}
1560    else:
1561        response.status_code = 404
1562        return {"message": "No pools found."}
1563
1564
1565@app.get("/control/pools/{pool_id}", tags=["pools", "control"])
1566def read_pool(
1567    *,
1568    pool_id: int = Path(..., title="The ID of pool to get", ge=1),
1569    response: Response,
1570    current_user: User = Depends(get_current_user),
1571):
1572    """
1573    Read catalog information abour just one pool by id.
1574    Built on console command _llist pool_
1575
1576    **Warning** Director does not support direct query by _id_ we query all pools and filter the result.
1577    Maybe more time consuming than expected in large settings.
1578    """
1579    allpools = read_all_pools(response, current_user)
1580    result = None
1581    for c in allpools["pools"]:
1582        if c["poolid"] == str(pool_id):
1583            result = c
1584            break
1585    if result:
1586        return result
1587    else:
1588        response.status_code = 404
1589        return {
1590            "message": "pool with pool ID {pool_id} not found".format(pool_id=pool_id)
1591        }
1592    return {"item_id": item_id}
1593
1594
1595### Schedules
1596
1597
1598@app.get("/configuration/schedules", tags=["schedules", "configuration"])
1599def read_all_schedules(
1600    *,
1601    response: Response,
1602    current_user: User = Depends(get_current_user),
1603    verbose: Optional[bareosBool] = Query("yes", title="Verbose output"),
1604):
1605    """
1606    Read all jobdef resources. Built on console command _show schedules_.
1607
1608    Needs at least Bareos Version >= 20.0.0
1609    """
1610    return show_configuration_items(
1611        response=response,
1612        current_user=current_user,
1613        itemType="schedules",
1614        verbose=verbose,
1615    )
1616
1617
1618@app.get(
1619    "/configuration/schedules/{schedules_name}", tags=["schedules", "configuration"]
1620)
1621def read_schedule_by_name(
1622    *,
1623    response: Response,
1624    current_user: User = Depends(get_current_user),
1625    schedules_name: str = Path(..., title="Client name to look for"),
1626    verbose: Optional[bareosBool] = Query("yes", title="Verbose output"),
1627):
1628    """
1629    Read all jobdef resources. Built on console command _show schedules_.
1630
1631    Needs at least Bareos Version >= 20.0.0
1632    """
1633    return show_configuration_items(
1634        response=response,
1635        current_user=current_user,
1636        itemType="schedules",
1637        byName=schedules_name,
1638        verbose=verbose,
1639    )
1640
1641
1642@app.post("/configuration/schedules", tags=["schedules", "configuration"])
1643def create_schedule(
1644    *,
1645    response: Response,
1646    current_user: User = Depends(get_current_user),
1647    scheduleDef: scheduleResource = Body(..., title="Name for new schedule"),
1648):
1649    """
1650    Create a new schedule resource.
1651    Console command used _configure add schedule_
1652    """
1653    return configure_add_standard_component(
1654        response=response,
1655        componentDef=scheduleDef,
1656        componentType="schedule",
1657        current_user=current_user,
1658    )
1659
1660
1661@app.put(
1662    "/control/schedules/enable/{schedule_name}",
1663    status_code=204,
1664    tags=["schedules", "control"],
1665)
1666def enable_schedule(
1667    *,
1668    schedule_name: str = Path(..., title="The schedule (name) to enable"),
1669    response: Response,
1670    current_user: User = Depends(get_current_user),
1671):
1672    (result, jsonMessage) = switch_resource(
1673        response=response,
1674        current_user=current_user,
1675        resourceName=schedule_name,
1676        componentType="schedule",
1677        enable=True,
1678    )
1679    if result:
1680        response.status_code = 204  # ok, but empty return-string
1681    else:
1682        response.status_code = 500
1683    return jsonMessage
1684
1685
1686@app.put(
1687    "/control/schedules/disable/{schedule_name}",
1688    status_code=204,
1689    tags=["schedules", "control"],
1690)
1691def disable_schedule(
1692    *,
1693    schedule_name: str = Path(..., title="The schedule (name) to disable"),
1694    response: Response,
1695    current_user: User = Depends(get_current_user),
1696):
1697    (result, jsonMessage) = switch_resource(
1698        response=response,
1699        current_user=current_user,
1700        resourceName=schedule_name,
1701        componentType="schedule",
1702        enable=False,
1703    )
1704    if result:
1705        response.status_code = 204  # ok, but empty return-string
1706    else:
1707        response.status_code = 500
1708    return jsonMessage
1709
1710
1711### Storages
1712
1713
1714@app.get("/configuration/storages", tags=["storages", "configuration"])
1715def read_all_storages(
1716    *,
1717    response: Response,
1718    current_user: User = Depends(get_current_user),
1719    verbose: Optional[bareosBool] = Query("yes", title="Verbose output"),
1720):
1721    """
1722    Read all jobdef resources. Built on console command _show storages_.
1723
1724    Needs at least Bareos Version >= 20.0.0
1725    """
1726    return show_configuration_items(
1727        response=response,
1728        current_user=current_user,
1729        itemType="storages",
1730        verbose=verbose,
1731    )
1732
1733
1734@app.get("/configuration/storages/{storages_name}", tags=["storages", "configuration"])
1735def read_storage_by_name(
1736    *,
1737    response: Response,
1738    current_user: User = Depends(get_current_user),
1739    storages_name: str = Path(..., title="Client name to look for"),
1740    verbose: Optional[bareosBool] = Query("yes", title="Verbose output"),
1741):
1742    """
1743    Read all jobdef resources. Built on console command _show storages_.
1744
1745    Needs at least Bareos Version >= 20.0.0
1746    """
1747    return show_configuration_items(
1748        response=response,
1749        current_user=current_user,
1750        itemType="storages",
1751        byName=storages_name,
1752        verbose=verbose,
1753    )
1754
1755
1756@app.post("/configuration/storage", tags=["storages", "configuration"])
1757def post_storage(
1758    *,
1759    storageDef: storageResource = Body(..., title="storage resource"),
1760    response: Response,
1761    current_user: User = Depends(get_current_user),
1762):
1763    """
1764    Create a new storage resource.
1765    Console command used: _configure add storage_
1766    """
1767    return configure_add_standard_component(
1768        response=response,
1769        componentDef=storageDef,
1770        componentType="storage",
1771        current_user=current_user,
1772    )
1773
1774
1775### Users, profiles, consoles
1776
1777
1778@app.get("/configuration/users", tags=["users", "configuration"])
1779def read_all_users(
1780    *,
1781    response: Response,
1782    current_user: User = Depends(get_current_user),
1783    verbose: Optional[bareosBool] = Query("yes", title="Verbose output"),
1784):
1785    """
1786    Read all users resources. Built on console command _show users_.
1787
1788    Needs at least Bareos Version >= 20.0.0
1789    """
1790    return show_configuration_items(
1791        response=response, current_user=current_user, itemType="users", verbose=verbose
1792    )
1793
1794
1795@app.get("/configuration/users/{users_name}", tags=["users", "configuration"])
1796def read_user_by_name(
1797    *,
1798    response: Response,
1799    current_user: User = Depends(get_current_user),
1800    users_name: str = Path(..., title="Client name to look for"),
1801    verbose: Optional[bareosBool] = Query("yes", title="Verbose output"),
1802):
1803    """
1804    Read all jobdef resources. Built on console command _show users_.
1805
1806    Needs at least Bareos Version >= 20.0.0
1807    """
1808    return show_configuration_items(
1809        response=response,
1810        current_user=current_user,
1811        itemType="users",
1812        byName=users_name,
1813        verbose=verbose,
1814    )
1815
1816
1817@app.post("/configuration/users", tags=["users", "configuration"])
1818def post_user(
1819    *,
1820    userDef: userResource = Body(..., title="user resource"),
1821    response: Response,
1822    current_user: User = Depends(get_current_user),
1823):
1824    """
1825    Create a new Bareos user resource.
1826    Console command used: _configure add user_
1827
1828    """
1829    return configure_add_standard_component(
1830        response=response,
1831        componentDef=userDef,
1832        componentType="user",
1833        current_user=current_user,
1834    )
1835
1836
1837@app.get("/configuration/profiles", tags=["profiles", "configuration"])
1838def read_all_profiles(
1839    *,
1840    response: Response,
1841    current_user: User = Depends(get_current_user),
1842    verbose: Optional[bareosBool] = Query("yes", title="Verbose output"),
1843):
1844    """
1845    Read all jobdef resources. Built on console command _show profiles_.
1846
1847    Needs at least Bareos Version >= 20.0.0
1848    """
1849    return show_configuration_items(
1850        response=response,
1851        current_user=current_user,
1852        itemType="profiles",
1853        verbose=verbose,
1854    )
1855
1856
1857@app.get("/configuration/profiles/{profiles_name}", tags=["profiles", "configuration"])
1858def read_client_by_name(
1859    *,
1860    response: Response,
1861    current_user: User = Depends(get_current_user),
1862    profiles_name: str = Path(..., title="Client name to look for"),
1863    verbose: Optional[bareosBool] = Query("yes", title="Verbose output"),
1864):
1865    """
1866    Read all jobdef resources. Built on console command _show profiles_.
1867
1868    Needs at least Bareos Version >= 20.0.0
1869    """
1870    return show_configuration_items(
1871        response=response,
1872        current_user=current_user,
1873        itemType="profiles",
1874        byName=profiles_name,
1875        verbose=verbose,
1876    )
1877
1878
1879@app.post("/configuration/profiles", tags=["users", "configuration"])
1880def post_profile(
1881    *,
1882    profileDef: profileResource = Body(..., title="profile resource"),
1883    response: Response,
1884    current_user: User = Depends(get_current_user),
1885):
1886    """
1887    Create a new Bareos profile resource.
1888    Console command used: _configure add profile_
1889
1890    """
1891    return configure_add_standard_component(
1892        response=response,
1893        componentDef=profileDef,
1894        componentType="profile",
1895        current_user=current_user,
1896    )
1897
1898
1899@app.get("/configuration/consoles", tags=["consoles", "configuration"])
1900def read_all_consoles(
1901    *,
1902    response: Response,
1903    current_user: User = Depends(get_current_user),
1904    verbose: Optional[bareosBool] = Query("yes", title="Verbose output"),
1905):
1906    """
1907    Read all jobdef resources. Built on console command _show consoles_.
1908
1909    Needs at least Bareos Version >= 20.0.0
1910    """
1911    return show_configuration_items(
1912        response=response,
1913        current_user=current_user,
1914        itemType="consoles",
1915        verbose=verbose,
1916    )
1917
1918
1919@app.get("/configuration/consoles/{consoles_name}", tags=["consoles", "configuration"])
1920def read_console_by_name(
1921    *,
1922    response: Response,
1923    current_user: User = Depends(get_current_user),
1924    consoles_name: str = Path(..., title="Console name to look for"),
1925    verbose: Optional[bareosBool] = Query("yes", title="Verbose output"),
1926):
1927    """
1928    Read all jobdef resources. Built on console command _show consoles_.
1929
1930    Needs at least Bareos Version >= 20.0.0
1931    """
1932    return show_configuration_items(
1933        response=response,
1934        current_user=current_user,
1935        itemType="consoles",
1936        byName=consoles_name,
1937        verbose=verbose,
1938    )
1939
1940
1941@app.post("/configuration/consoles", tags=["users", "configuration"])
1942def post_console(
1943    *,
1944    consoleDef: consoleResource = Body(..., title="console resource"),
1945    response: Response,
1946    current_user: User = Depends(get_current_user),
1947):
1948    """
1949    Create a new Bareos console resource.
1950    Console command used: _configure add console_
1951
1952    """
1953    return configure_add_standard_component(
1954        response=response,
1955        componentDef=consoleDef,
1956        componentType="console",
1957        current_user=current_user,
1958    )
1959
1960
1961### Director
1962
1963
1964@app.get("/control/directors/version", tags=["directors", "control"])
1965def read_director_version(
1966    *, response: Response, current_user: User = Depends(get_current_user)
1967):
1968    """
1969    Read director version. Command used: _version_
1970    """
1971    result = None
1972    dirCommand = "version"
1973    try:
1974        result = current_user.jsonDirector.call(dirCommand)
1975    except Exception as e:
1976        raise HTTPException(
1977            status_code=500, detail="Could not read version from director"
1978        )
1979    if result and "version" in result:
1980        return result["version"]
1981    else:
1982        response.status_code = 404
1983        return {"message": "No version info returned"}
1984
1985
1986@app.get("/control/directors/time", tags=["directors", "control"])
1987def read_director_time(
1988    *, response: Response, current_user: User = Depends(get_current_user)
1989):
1990    """
1991    Read director time. Command used: _time_
1992    """
1993    result = None
1994    dirCommand = "time"
1995    try:
1996        result = current_user.jsonDirector.call(dirCommand)
1997    except Exception as e:
1998        response.status_code = 500
1999        return {
2000            "message": "Could not read director time %s. Message: '%s'"
2001            % (current_user.directorName, e)
2002        }
2003    if result and "time" in result:
2004        return result["time"]
2005    else:
2006        response.status_code = 404
2007        return {"message": "No time info returned"}
2008
2009
2010@app.put("/control/directors/reload", tags=["directors", "control"])
2011def read_director_time(
2012    *, response: Response, current_user: User = Depends(get_current_user)
2013):
2014    """
2015    Reload director configuration from files. Command used: _reload_
2016    """
2017    result = None
2018    dirCommand = "reload"
2019    try:
2020        result = current_user.jsonDirector.call(dirCommand)
2021    except Exception as e:
2022        response.status_code = 500
2023        return {
2024            "message": "Could not reload director %s. Message: '%s'"
2025            % (current_user.directorName, e)
2026        }
2027    if result and "reload" in result:
2028        return result["reload"]
2029    else:
2030        response.status_code = 500
2031        return result
2032