diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4e78060..59ce702 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -55,7 +55,7 @@ test_backend: - export no_proxy="192.168.104.7" - apt update - apt install sqlite3 - - export ENVIRONMENT='DEV' + - export ENVIRONMENT='TEST' - export KEYRING_DNS_SECRET=$KEYRING_DNS_SECRET - export PROXMOX_API_KEY_NAME=$PROXMOX_API_KEY_NAME - export PROXMOX_API_KEY=$PROXMOX_API_KEY diff --git a/README.md b/README.md index ab828bd..6fffa21 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,13 @@ KEYRING_DNS_SECRET : clé pour utiliser le ddns (ajouter / supprimer des entrée PROXMOX_API_KEY_NAME : nom de la clé pour accéder à l'api proxmox PROXMOX_API_KEY : clé pour utiliser l'api proxmox PROXMOX_BACK_DB : mysql:// lien vers la db contenant le passwd, user, nom de la database et bien sûr ip de celle-ci +ENVIRONMENT="DEV" ou "TEST" (ou "PROD") ``` +L'environnement conditionne certains composants de l'application. +1. "PROD" est réservé à l'execution de l'application dans un environnement de production. C'est par exemple au sein de cet environment que les cron jobs s'executeront. Il est impératif que seul l'env de PROD soit en charge de ce genre d'opérations +2. "DEV" désactive certaines fonctionnalités réservées à la prod, comme les cron jobs. Mais il utilise la même pas de données la production. C'est l'environment à utiliser en local ou sur hosting-dev +3. "TEST" est l'environment utilisé pour effectuer les tests unitaires et d'intégrations du backend. Il déploie un base de donnée particulière réservée aux tests. + - vous ensuite devez vous rendre dans backend/ et exécuter la commande `python3 -m proxmox_api`. Le serveur se lance alors. *Assurez vous qu'il est joignable via le port 8080 de votre machine pour qu'il puisse être joint par le `frontend`* diff --git a/backend/proxmox_api/__main__.py b/backend/proxmox_api/__main__.py index d5b8a99..5e55838 100644 --- a/backend/proxmox_api/__main__.py +++ b/backend/proxmox_api/__main__.py @@ -8,11 +8,11 @@ print("config = ", config.ENV) -if config.ENV == "DEV": +if config.ENV == "TEST": print("**************************************************") print("**************************************************") print("**************************************************") - print("************* ENTERING IN DEV MODE ***************") + print("************* ENTERING IN TEST MODE ***************") print("********* NOT DESIGNED FOR PRODUCTION ************") print("**************************************************") print("**************************************************") @@ -32,27 +32,24 @@ def create_app(): def conf_jobs(app): + app.app.config['JOBS'] = JOBS app.app.config['SCHEDULER_API_ENABLE'] = False - - - - - ## init db app, scheduler = create_app() -#JOBS = [ -# { -# "id": "update_vm_ips", -# "func": "proxmox_api.proxmox:update_vm_ips_job", -# "args": (app.app,), -# "trigger": "interval", -# "seconds": 120, -# } -# ] -# -#conf_jobs(app) +JOBS = [ + { + "id": "stop_expired_vm", + "func": "proxmox_api.proxmox:stop_expired_vm", + "args": (app.app,), + "trigger": "interval", + 'seconds': 86400,# 1 day + } + ] + +if config.ENV == "PROD": + conf_jobs(app) db.init_app(app.app) diff --git a/backend/proxmox_api/config/configuration.py b/backend/proxmox_api/config/configuration.py index c774400..06d0801 100644 --- a/backend/proxmox_api/config/configuration.py +++ b/backend/proxmox_api/config/configuration.py @@ -31,10 +31,13 @@ """Be sure to set "set global log_bin_trust_function_creators=1"; in the database if mysql""" -if os.environ.get('ENVIRONMENT') == 'DEV': - ENV = "DEV" +if os.environ.get('ENVIRONMENT') == 'TEST': + ENV = "TEST" #DATABASE_URI = os.environ.get('PROXMOX_BACK_DB_DEV') DATABASE_URI = 'sqlite:///proxmox_dev.db' +elif os.environ.get('ENVIRONMENT') == 'DEV': + ENV = "DEV" + DATABASE_URI = os.environ.get('PROXMOX_BACK_DB') else : ENV = "PROD" DATABASE_URI = os.environ.get('PROXMOX_BACK_DB') diff --git a/backend/proxmox_api/config/vm_creation_status.json b/backend/proxmox_api/config/vm_creation_status.json index 0967ef4..7841ea8 100644 --- a/backend/proxmox_api/config/vm_creation_status.json +++ b/backend/proxmox_api/config/vm_creation_status.json @@ -1 +1 @@ -{} +{"999": {"status": "creating"}, "999": {"status": "creating"}} \ No newline at end of file diff --git a/backend/proxmox_api/controllers/default_controller.py b/backend/proxmox_api/controllers/default_controller.py index fac682d..4e808ba 100644 --- a/backend/proxmox_api/controllers/default_controller.py +++ b/backend/proxmox_api/controllers/default_controller.py @@ -204,8 +204,8 @@ def delete_vm_id(vmid): # noqa: E501 if freezeAccountState >= 3 and not admin: # if freeze state 1 or 2 user still have access to proxmox return {"status": "cotisation expired"}, 403 - node = proxmox.get_node_from_vm(vmid) - if not node : #doesn't exist + node,status = proxmox.get_node_from_vm(vmid) + if status != 200 : #doesn't exist return {"error": "VM doesn't exist"}, 404 Thread(target=delete_vm_in_thread, args=(vmid, user_id, node,False,)).start() @@ -251,8 +251,8 @@ def delete_vm_in_thread(vmid, user_id, node="", dueToError=False): #if freezeAccountState >= 3 and not admin: # if freeze state 1 or 2 user still have access to proxmox # return {"status": "cotisation expired"}, 403 - node = proxmox.get_node_from_vm(vmid) - if not node: + node,status = proxmox.get_node_from_vm(vmid) + if status != 200: return {"status": "vm not exists"}, 404 if vmid in map(int, proxmox.get_vm(user_id)[0]): return proxmox.delete_vm(vmid, node) @@ -420,12 +420,14 @@ def get_vm_id(vmid): # noqa: E501 return {"error": "Unknown vm status"}, 400 - node = proxmox.get_node_from_vm(vmid) + node,status = proxmox.get_node_from_vm(vmid) + + if not admin and dbfct.get_vm_userid(vmid) != user_id : # if not admin, we check if the user is the owner of the vm return {'error' : "Forbidden"} , 403 - elif node == None and not admin: # exist in the db but not in proxmox. It's a error + elif status != 200 and not admin: # exist in the db but not in proxmox. It's a error return {"error": "VM not found in proxmox"}, 500 - elif node == None and admin: + elif status != 200 and admin: return {'error' : "VM no found"} , 404 @@ -722,8 +724,8 @@ def patch_vm(vmid, body=None): # noqa: E501 user_id = slugify(cas['sub'].replace('_', '-')) if admin or dbfct.get_vm_userid(vmid) == user_id : # if not admin, we check if the user is the owner of the vm - node = proxmox.get_node_from_vm(vmid) - if not node: + node,status = proxmox.get_node_from_vm(vmid) + if status != 200: return {"status": "vm not exists"}, 404 if requetsBody.status == "start": return proxmox.start_vm(vmid, node) @@ -733,6 +735,12 @@ def patch_vm(vmid, body=None): # noqa: E501 return proxmox.stop_vm(vmid, node) elif requetsBody.status == "switch_autoreboot": return proxmox.switch_autoreboot(vmid, node) + elif requetsBody.status == "transfering_ownership": + if admin : + new_owner = slugify(requetsBody.user.replace('_', '-')) + return proxmox.transfer_ownership(vmid, new_owner) + else: + return {"status": "Permission denied"}, 403 else: return {"status": "uknown status"}, 500 else: diff --git a/backend/proxmox_api/db/db_functions.py b/backend/proxmox_api/db/db_functions.py index 40808d4..bdf108a 100644 --- a/backend/proxmox_api/db/db_functions.py +++ b/backend/proxmox_api/db/db_functions.py @@ -42,6 +42,11 @@ def get_user_list(user_id=None, searchItem = None): # filter is for the user nam else : return User.query.all() +def update_vm_userid(vmid, userid): + vm = Vm.query.filter_by(id=vmid).first() + vm.userId = userid + db.session.commit() + # Return all the VM of an user def get_vm_list(user_id=""): if user_id != "": # dans ce cas on affiche ce qui est lié à l'user @@ -217,4 +222,15 @@ def setNeedToBeRestored(vmid, needToBeRestored): vm.needToBeRestored = needToBeRestored db.session.commit() + +# Return expired users with a freezeState >= minimumFreezeState +def get_expired_users(minimumFreezeState = 1): + list = [] + for user in User.query.all(): + if user.freezeState !=None: + state = user.freezeState.split(".")[0] + if int(state) >= minimumFreezeState: + list.append(user.id) + return list + ####### diff --git a/backend/proxmox_api/db/db_models.py b/backend/proxmox_api/db/db_models.py index 70e1659..b8cbfa1 100644 --- a/backend/proxmox_api/db/db_models.py +++ b/backend/proxmox_api/db/db_models.py @@ -38,7 +38,7 @@ class History(db.Model): date = db.Column(db.TIMESTAMP,default=db.func.current_timestamp(), nullable=False) -if ENV != "DEV": +if ENV != "TEST": ### Trigger for ip tracking TRIGGER_CREATION = """ CREATE TRIGGER history_insert_vm AFTER UPDATE ON vm diff --git a/backend/proxmox_api/proxmox.py b/backend/proxmox_api/proxmox.py index 6651cab..d9809d0 100644 --- a/backend/proxmox_api/proxmox.py +++ b/backend/proxmox_api/proxmox.py @@ -195,6 +195,7 @@ def create_vm(name, vm_type, user_id, password="no", vm_user="", main_ssh_key="n template_node = "" try: + print("oui") template_id = -1 if vm_type == "bare_vm": template_id = 10003 @@ -214,23 +215,27 @@ def create_vm(name, vm_type, user_id, password="no", vm_user="", main_ssh_key="n database.add_ip_to_history(ip, next_vmid, user_id) else: return {"error": "error, can not create more VMs"}, 500 + for vm in proxmox.cluster.resources.get(type="vm"): if vm["vmid"] == template_id: template_node = vm["node"] + print("template_node", template_node) + print("template_id", template_id) proxmox.nodes(template_node).qemu(template_id).clone.create( name=name, newid=next_vmid, target=node, full=1, + storage="tmp_replicated_2_times" ) except Exception as e: - delete_from_db(next_vmid) - delete_from_proxmox(next_vmid, node) logging.error("Problem in create_vm(" + str(next_vmid) + ") when cloning: " + str(e)) print("Problem in create_vm(" + str(next_vmid) + ") when cloning: " + str(e)) + delete_from_db(next_vmid) + delete_from_proxmox(next_vmid, node) return {"error": "Impossible to create the VM (cloning)"}, 500 @@ -238,6 +243,7 @@ def create_vm(name, vm_type, user_id, password="no", vm_user="", main_ssh_key="n print("Problem while updating the vm status") return {"error": "Impossible to update the VM status"}, 500 Thread(target=config_vm, args=(next_vmid, node, password, vm_user, main_ssh_key,ip, )).start() + return {"vmId": next_vmid}, 201 @@ -246,7 +252,7 @@ def create_vm(name, vm_type, user_id, password="no", vm_user="", main_ssh_key="n When the VM is up, the password, vm user name and ssh key are set up """ def config_vm(vmid, node, password, vm_user,main_ssh_key, ip): - + print("configuring vm") sync = False vm = proxmox.nodes(node).qemu(vmid) while not sync: # Synchronisation @@ -267,11 +273,11 @@ def config_vm(vmid, node, password, vm_user,main_ssh_key, ip): cipassword=password ) except Exception as e: + logging.error("Problem in create_vm(" + str(vmid) + ") when setting password: " + str(e)) + print("Problem in create_vm(" + str(vmid) + ") when setting password: " + str(e)) delete_from_db(vmid) delete_from_proxmox(vmid, node) util.update_vm_state(vmid,"An error occured while setting your password (vmid ="+str(vmid) +")", errorCode=500) - logging.error("Problem in create_vm(" + str(vmid) + ") when setting password: " + str(e)) - print("Problem in create_vm(" + str(vmid) + ") when setting password: " + str(e)) try: @@ -397,7 +403,9 @@ def reboot_vm(vmid, node): return {"status": "An error occur while rebooting the vm"}, 500 def renew_ip(vmid): - node = get_node_from_vm(vmid) + node, status = get_node_from_vm(vmid) + if status != 200: + return node, status try: ip = database.get_vm_ip(vmid) proxmox.nodes(node).qemu(vmid).config.post( @@ -411,7 +419,9 @@ def renew_ip(vmid): return {"status": "error updating your ip address."}, 500 def update_vm_credentials(vmid,username, password, sshKey): - node = get_node_from_vm(vmid) + node,status = get_node_from_vm(vmid) + if status != 200: + return node, status try : ip = database.get_vm_ip(vmid) proxmox.nodes(node).qemu(vmid).config.post( @@ -493,9 +503,12 @@ def get_vm(user_id = 0, search=None): for user in user_filtered: vm_filtered_list += database.get_vm_list(user_id = user.id) start = time.time() - vm_list = database.get_vm_list() # only id + vm_list = database.get_vm_list() # get all vm vut only id for vmid in vm_list: - node = get_node_from_vm(vmid) + node, status = get_node_from_vm(vmid) + if status == 200: + if status != 200: + return node, status if vmid not in vm_filtered_list : infos,_ = get_vm_name(vmid, node) name = infos["name"] @@ -545,7 +558,7 @@ def get_node_from_vm(vmid): if node == "": return {"get_node": "Vm not found"}, 404 else : - return node + return node, 200 else: return {"get_node": "Vmid incorrect"}, 404 @@ -744,7 +757,9 @@ def get_user_ip_list(user_id) : vm_id_list = database.get_vm_list(user_id) ip_list = [] # ip of the user for vmid in vm_id_list: - node = get_node_from_vm(vmid) + node,status = get_node_from_vm(vmid) + if status != 200: + return None vm_ip, status = get_vm_ip(vmid, node) if status == 200: ip_list += vm_ip["vm_ip"] @@ -822,73 +837,40 @@ def check_update_cotisation(username, createEntry=False): print("check cotisation of", username) #headers = {"Authorization": req_headers} - headers = {"X-API-KEY": configuration.ADH6_API_KEY} - #print("https://adh6.minet.net/api/member/?limit=25&filter%5Busername%5D="+str(username)+"&only=id,username") - userInfoJson = util.adh6_search_user(username, headers) - print(userInfoJson) - if (len(userInfoJson) > 1 ): + account, status = util.get_adh6_account(username) + if (account is None): return {"error": "Impossible to retrieve the user info"}, 404 - if userInfoJson is None or userInfoJson == []: # not found - if "-" in username: - print("ERROR : the user " + username + " is not found in ADH6. Try with", end='') - new_username = username.replace("-","_") # hosting replace by default _ with -. So we try if not found - print("'"+new_username+"'") - return check_update_cotisation(new_username) - - elif "_" in username: # same - print("ERROR : the user " + username + " is not found in ADH6. Try with", end='') - new_username = username.replace("_",".").strip() # hosting replace by default _ with -. So we try if not found - print("'"+new_username+"'") - return check_update_cotisation(new_username) - else : - print("ERROR : the user " , username , " failed to be retrieved :" , userInfoJson) - return {"error" : "the user " + username + " failed to be retrieved"}, 404 - else : - username = username.replace("_", "-") # hosting replace by default _ and . with -. - userId = userInfoJson[0] - membership_dict = util.check_adh6_membership(headers, userId) - print(membership_dict) - today = date.today() - if "ip" not in membership_dict: # Cotisation expired - #print(username , "cotisation expired", membership.json()) - print(username , "cotisation expired (no ip)") - - #return expiredCotisation(username, userEmail) #, datetime.strptime(membership_dict["departureDate"], "%Y-%m-%d").date()) - + username = username.replace("_", "-") # hostingreplace by default _ and . with -. + print("Adh6 account", account) + today = date.today() + if "ip" not in account: # Cotisation expired + #print(username , "cotisation expired", membership.json()) + print(username , "cotisation expired (no ip)") + + #return expiredCotisation(username, userEmail) #, datetime.strptime(account["departureDate"], "%Y-%m-%d").date()) + + status = database.getFreezeState(username) + if status is None: + status = '1' + database.updateFreezeState(username, "1.0") + return {"freezeState": status}, 200 + + else : # we check anyway if the departure date is inthe future + #print(membership.json()["ip"]) + #print(membership.json()["departureDate"], end='\n\n') + departureDate = datetime.strptime(account["departureDate"], "%Y-%m-%d").date() + if departureDate < today: # Cotisation expired: + print(username , "cotisation expired (departure date)") status = database.getFreezeState(username) if status is None: status = '1' database.updateFreezeState(username, "1.0") return {"freezeState": status}, 200 - else : # we check anyway if the departure date is in the future - #print(membership.json()["ip"]) - #print(membership.json()["departureDate"], end='\n\n') - departureDate = datetime.strptime(membership_dict["departureDate"], "%Y-%m-%d").date() - if departureDate < today: # Cotisation expired: - print(username , "cotisation expired (departure date)") - status = database.getFreezeState(username) - if status is None: - status = '1' - database.updateFreezeState(username, "1.0") - return {"freezeState": status}, 200 - - else : - print(username, "cotisation up to date") - database.updateFreezeState(username, "0.0") - return {"freezeState": "0"}, 200 - - - - -##### -# For the jenkins script -#### - - - - - + else : + print(username, "cotisation up to date") + database.updateFreezeState(username, "0.0") + return {"freezeState": "0"}, 200 def next_available_vmid():# determine the next available vmid from both db and proxmox @@ -899,4 +881,52 @@ def next_available_vmid():# determine the next available vmid from both db and p next_vmid_db = database.getNextVmID(next_vmid_db) is_vmid_available_prox = is_vmid_available_cluster(next_vmid_db) - return next_vmid_db \ No newline at end of file + return next_vmid_db + + + """_summary_ : This function is called by the job to stop expired vm when the account freeze state is 2.x or 3.1 + """ +def stop_expired_vm(app): + print("Executing expired vm jobs ...") + with app.app_context(): # Needs application context + users = database.get_expired_users(minimumFreezeState=2) + + for user in users: + print("Checking vm for user : ", user) + userVms = database.get_vm_list(user_id=user) + for vmid in userVms: + node, _ = get_node_from_vm(vmid) + status,_ = get_proxmox_vm_status(vmid, node) + if status["status"] == "running": + print("Stopping vm", vmid , "which was running") + stop_vm(vmid, node) + print("VMs stopped for user", user) + +# Transfer the ownership of a vm to another user. +def transfer_ownership(vmid, newowner): + if newowner == "" or newowner == None : + return {"error": "No login given"}, 400 + + user = database.get_user_list(user_id=newowner) + if user == None: # We search it in ADH6 + account, status = util.get_adh6_account(newowner) + if status != 200: + return account, status + if account is None: + return {"error": "User not found"}, 404 + else: + print("account", account) + userid = account["username"] + # We add the user in the database + database.add_user(userid) + else: + userid = newowner + userVms = database.get_vm_list(user_id=userid) + if len(userVms) >= 3: + return {"error": "User already has 3 VMs"}, 400 + database.update_vm_userid(vmid, userid) + ip = database.get_vm_ip(vmid) + database.add_ip_to_history(ip, vmid, userid) + return {"status": "ok"}, 201 + + \ No newline at end of file diff --git a/backend/proxmox_api/swagger/swagger.yaml b/backend/proxmox_api/swagger/swagger.yaml index 6d71ce8..54952e1 100644 --- a/backend/proxmox_api/swagger/swagger.yaml +++ b/backend/proxmox_api/swagger/swagger.yaml @@ -119,7 +119,7 @@ paths: type: string example: "105" requestBody: - description: VM to update + description: VM to update. Avalaible status are start, reboot, stop, switch_autoreboot, transfer_ownership content: application/json: schema: diff --git a/backend/proxmox_api/util.py b/backend/proxmox_api/util.py index 45d2bb6..f74bba8 100644 --- a/backend/proxmox_api/util.py +++ b/backend/proxmox_api/util.py @@ -311,7 +311,7 @@ def create_app(): def check_cas_token(headers): - #if config.ENV == "DEV": + #if config.ENV == "TEST": # if headers["Fake-User"] == "admin": # return 200, {'sub': 'fake-admin', "attributes" : {"memberOf" : 'cn=cluster-hosting,ou=groups,dc=minet,dc=net'}} # else : @@ -319,15 +319,40 @@ def check_cas_token(headers): #elif config.ENV == "PROD": autorization = {"Authorization": headers["Authorization"]} r = requests.get("https://cas.minet.net/oidc/profile", headers=autorization) - print("return =", r.json()) return r.status_code, r.json() -def check_adh6_membership(headers, userId): - response = requests.get("https://adh6.minet.net/api/member/"+str(userId), headers=headers) # memership info - return response.json() +def get_adh6_account(username): + headers = {"X-API-KEY": config.ADH6_API_KEY} + #print("https://adh6.minet.net/api/member/?limit=25&filter%5Busername%5D="+str(username)+"&only=id,username") + userInfoJson = adh6_search_user(username, headers) + print(userInfoJson) + if userInfoJson is None or userInfoJson == []: # not found + if "-" in username: + print("ERROR : the user " + username + " is not found in ADH6. Try with", end='') + new_username = username.replace("-","_") # hosting replace by default _ with -. So we try if not found + print("'"+new_username+"'") + return get_adh6_account(new_username) + + elif "_" in username: # same + print("ERROR : the user " + username + " is not found in ADH6. Try with", end='') + new_username = username.replace("_",".").strip() # hosting replace by default _ with -. So we try if not found + print("'"+new_username+"'") + return get_adh6_account(new_username) + else : + print("ERROR : the user " , username , " failed to be retrieved :" , userInfoJson) + return {"error" : "the user " + username + " failed to be retrieved"}, 404 + else : + account = None + for id in userInfoJson: + accountJson = requests.get("https://adh6.minet.net/api/member/"+str(id), headers=headers) # memership info + tmp_account = accountJson.json() + if tmp_account["username"] == username: + account = tmp_account + print("account : ", account) + return account, 200 def adh6_search_user(username, headers): response = requests.get("https://adh6.minet.net/api/member/?limit=25&terms="+str(username), headers=headers) # [id], from ADH6 diff --git a/backend/test/conftest.py b/backend/test/conftest.py index 9aabeb7..970dd59 100644 --- a/backend/test/conftest.py +++ b/backend/test/conftest.py @@ -10,13 +10,13 @@ #@pytest.fixture #def client(): -# os.environ.update({"ENVIRONNMENT": "dev"}) +# os.environ.update({"ENVIRONNMENT": "TEST"}) # # @pytest.fixture() def init_user_database(): - if configuration.ENV != "DEV": - raise Exception("You must set the environnement to DEV to run the tests") + if configuration.ENV != "TEST": + raise Exception("You must set the environnement to TEST to run the tests") db = SQLAlchemy() db.init_app(flask_app.app) with flask_app.app.app_context(): @@ -62,8 +62,8 @@ def create_post_model(user): @pytest.fixture() def init_vm_database(): - if configuration.ENV != "DEV": - raise Exception("You must set the environnement to DEV to run the tests") + if configuration.ENV != "TEST": + raise Exception("You must set the environnement to TEST to run the tests") db = SQLAlchemy() db.init_app(flask_app.app) with flask_app.app.app_context(): @@ -107,6 +107,51 @@ def create_vm_model(vms): db.session.commit() +@pytest.fixture() +def init_expired_vm_database(): + if configuration.ENV != "TEST": + raise Exception("You must set the environnement to TEST to run the tests") + db = SQLAlchemy() + db.init_app(flask_app.app) + with flask_app.app.app_context(): + try: + db.session.query(model.Vm).delete() + except: + print("No VM to delete") + db.create_all() + + # List of test VM + test_vms = [ + {"id" : 1, "userId" : "user-1", "type":"bare", "ip" : None, "mac" : None,"needToBeRestored" : False}, + {"id" : 2, "userId" : "expired-user-1", "type":"bare", "ip" : None, "mac" : None,"needToBeRestored" : False}, + {"id" : 3, "userId" : "expired-user-2", "type":"bare", "ip" : None, "mac" : None,"needToBeRestored" : False}, + {"id" : 4, "userId" : "expired-user-3", "type":"bare", "ip" : None, "mac" : None,"needToBeRestored" : False}, + {"id" : 5, "userId" : "expired-user-2", "type":"bare", "ip" : None, "mac" : None,"needToBeRestored" : False} + ] + + + # Convert the list of dictionaries to a list of User objects + def create_user_model(user): + return model.User(**user) + def create_vm_model(vms): + return model.Vm(**vms) + + # Create a list of objects + mapped_vms = map(create_vm_model, test_vms) + t_vms = list(mapped_vms) + + db.session.add_all(t_vms) + + # Commit the changes for the users + db.session.commit() + + yield db # this is where the testing happens! + db.session.remove() # looks like db.session.close() would work as well + # Drop the database table + #model.User.query.delete() + db.session.query(model.User).delete() + db.session.query(model.Vm).delete() + db.session.commit() @pytest.fixture() def proxmoxAPI(): diff --git a/backend/test/integration/test_vm_life.py b/backend/test/integration/test_vm_life.py index 2100229..5a28840 100644 --- a/backend/test/integration/test_vm_life.py +++ b/backend/test/integration/test_vm_life.py @@ -12,16 +12,15 @@ def test_old_vm_deletion(init_vm_database): """Test in charge of destroying all vm test created in the past. """ - node = proxmox.get_node_from_vm(VMID) + node,status = proxmox.get_node_from_vm(VMID) print(node) app = util.create_app() db = SQLAlchemy() db.init_app(app.app) doesVMexist = False with app.app.app_context(): - if len(node) >= 2: - if node[1] != 404: - doesVMexist = True + if status == 200: + doesVMexist = True if doesVMexist: assert node == "kars" or node == "wammu" r = proxmox.delete_from_proxmox(VMID, node) @@ -69,9 +68,10 @@ def test_vm_start(): db = SQLAlchemy() db.init_app(app.app) with app.app.app_context(): - node = proxmox.get_node_from_vm(VMID) - body, status = proxmox.start_vm(VMID, node) - assert status == 201 + node,status_node = proxmox.get_node_from_vm(VMID) + body, status_start = proxmox.start_vm(VMID, node) + assert status_node == 200 + assert status_start == 201 # If previous test fail, we do not try to start it @pytest.mark.dependency(name="stop", depends=["clean", "creation", "start"]) @@ -82,9 +82,10 @@ def test_vm_stop(): db = SQLAlchemy() db.init_app(app.app) with app.app.app_context(): - node = proxmox.get_node_from_vm(VMID) - body, status = proxmox.stop_vm(VMID, node) - assert status == 201 + node,status_node = proxmox.get_node_from_vm(VMID) + body, status_stop = proxmox.stop_vm(VMID, node) + assert status_node == 200 + assert status_stop == 201 # If previous test fail, we do not try to start it @pytest.mark.dependency(name="delete", depends=["clean", "creation", "start", "stop"]) @@ -95,9 +96,10 @@ def test_vm_deletion(): db = SQLAlchemy() db.init_app(app.app) with app.app.app_context(): - node = proxmox.get_node_from_vm(VMID) + node,status_node = proxmox.get_node_from_vm(VMID) isProxmoxDeleted = proxmox.delete_from_proxmox(VMID, node) isDbDeleted = proxmox.delete_from_db(VMID) + assert status_node == 200 assert isProxmoxDeleted assert isDbDeleted diff --git a/backend/test/test_account_state.py b/backend/test/test_account_state.py index a30fbf0..bb348a5 100644 --- a/backend/test/test_account_state.py +++ b/backend/test/test_account_state.py @@ -59,18 +59,17 @@ def test_expired_account_freezed_1(monkeypatch, init_user_database): freeze state of 1 """ - def fake_adh6_check(username, headers): - return {} - def fake_adh6_search_user(username, headers): - return [0] + username = "expired-user-1" + def fake_get_adh6_account(username): + return {'username' : username},200 + app = util.create_app() db = SQLAlchemy() db.init_app(app.app) with app.app.app_context(): - monkeypatch.setattr(util, 'check_adh6_membership', fake_adh6_check) - monkeypatch.setattr(util, 'adh6_search_user', fake_adh6_search_user) - r = proxmox.get_freeze_state("expired-user-1") + monkeypatch.setattr(util, 'get_adh6_account', fake_get_adh6_account) + r = proxmox.get_freeze_state(username) dict,status_code = r assert status_code == 200 assert dict['freezeState'] == '1' @@ -81,18 +80,16 @@ def test_expired_account_freezed_2(monkeypatch, init_user_database): freeze state of 2 """ - def fake_adh6_check(username, headers): - return {} - def fake_adh6_search_user(username, headers): - return [0] + username = "expired-user-2" + def fake_get_adh6_account(username): + return {"username":username},200 app = util.create_app() db = SQLAlchemy() db.init_app(app.app) with app.app.app_context(): - monkeypatch.setattr(util, 'check_adh6_membership', fake_adh6_check) - monkeypatch.setattr(util, 'adh6_search_user', fake_adh6_search_user) - r = proxmox.get_freeze_state("expired-user-2") + monkeypatch.setattr(util, 'get_adh6_account', fake_get_adh6_account) + r = proxmox.get_freeze_state(username) dict,status_code = r assert status_code == 200 assert dict['freezeState'] == '2' @@ -103,18 +100,17 @@ def test_expired_account_freezed_3(monkeypatch, init_user_database): freeze state of 3 """ - def fake_adh6_check(username, headers): - return {} - def fake_adh6_search_user(username, headers): - return [0] + username = "expired-user-3" + def fake_get_adh6_account(username): + return {'username' : username},200 app = util.create_app() db = SQLAlchemy() db.init_app(app.app) with app.app.app_context(): - monkeypatch.setattr(util, 'check_adh6_membership', fake_adh6_check) - monkeypatch.setattr(util, 'adh6_search_user', fake_adh6_search_user) - r = proxmox.get_freeze_state("expired-user-3") + monkeypatch.setattr(util, 'get_adh6_account', fake_get_adh6_account) + + r = proxmox.get_freeze_state(username) dict,status_code = r assert status_code == 200 assert dict['freezeState'] == '3' @@ -125,18 +121,16 @@ def test_expired_account_freezed_4(monkeypatch,init_user_database): freeze state of 4 """ - def fake_adh6_check(username, headers): - return {} - def fake_adh6_search_user(username, headers): - return [0] + username = "expired-user-4" + def fake_get_adh6_account(username): + return {'username' : username}, 200 app = util.create_app() db = SQLAlchemy() db.init_app(app.app) with app.app.app_context(): - monkeypatch.setattr(util, 'check_adh6_membership', fake_adh6_check) - monkeypatch.setattr(util, 'adh6_search_user', fake_adh6_search_user) - r = proxmox.get_freeze_state("expired-user-4") + monkeypatch.setattr(util, 'get_adh6_account', fake_get_adh6_account) + r = proxmox.get_freeze_state(username) dict,status_code = r assert status_code == 200 assert dict['freezeState'] == '4' @@ -148,10 +142,9 @@ def test_new_account_to_be_checked(monkeypatch, init_user_database): Account that must be checked by adh6 """ - def fake_adh6_check(username, headers): - return {'ip': "127.0.0.1", "departureDate" : "2199-01-01"} - def fake_adh6_search_user(username, headers): - return [0] + username = "new-user-to-be-checked" + def fake_get_adh6_account(username): + return {'ip': "127.0.0.1", "departureDate" : "2199-01-01", 'username' : username}, 200 @@ -159,9 +152,8 @@ def fake_adh6_search_user(username, headers): db = SQLAlchemy() db.init_app(app.app) with app.app.app_context(): - monkeypatch.setattr(util, 'check_adh6_membership', fake_adh6_check) - monkeypatch.setattr(util, 'adh6_search_user', fake_adh6_search_user) - r = proxmox.get_freeze_state("new-user-to-be-checked") + monkeypatch.setattr(util, 'get_adh6_account', fake_get_adh6_account) + r = proxmox.get_freeze_state(username) dict,status_code = r assert status_code == 200 assert dict['freezeState'] == '0' \ No newline at end of file diff --git a/backend/test/test_default_api.py b/backend/test/test_default_api.py index 9b329e8..01a8887 100644 --- a/backend/test/test_default_api.py +++ b/backend/test/test_default_api.py @@ -15,7 +15,7 @@ def fake_check_cas_token_fail(headers): def fake_check_cas_admin(headers): return 200, {"sub": "admin", "attributes" : {"memberOf" : 'cn=cluster-hosting,ou=groups,dc=minet,dc=net'}} def fake_get_node_from_vm(vmid): - return "wammu" + return "wammu",200 def fake_get_proxmox_vm_status(vmid, node): return "running" def fake_get_vm_config(vmid, node): @@ -150,6 +150,11 @@ def fake_stop_vm(vmide, node): def fake_switch_autoreboot(vmide, node): return {"status": "switch autoreboot OK"},200 +def fake_transfer_ownership(vmid, new_owner): + if new_owner == "" or new_owner == None : + return {"error": "No login given"}, 400 + return {"status": "OK"},200 + def test_valid_patch_vm_start(client, init_user_database, init_vm_database, monkeypatch): monkeypatch.setattr(util, "check_cas_token", fake_check_cas_token) monkeypatch.setattr(proxmox, "get_node_from_vm", fake_get_node_from_vm) @@ -223,6 +228,26 @@ def test_admin_patch_vm(client, init_user_database, init_vm_database, monkeypatc assert vm2.status_code == 200 assert vm2.json == {"status": "start OK"} +# Try to transfert the VM as an admin +def test_admin_transfer_ownership(client, init_user_database, init_vm_database, monkeypatch): + monkeypatch.setattr(util, "check_cas_token", fake_check_cas_admin) + monkeypatch.setattr(proxmox, "get_node_from_vm", fake_get_node_from_vm) + monkeypatch.setattr(proxmox, "transfer_ownership", fake_transfer_ownership) + + vm1 = client.patch('/api/1.0.0/vm/3', headers={'Content-Type': 'application/json', "Authorization" : "Bearer AT-TEST"}, json={"status": "transfering_ownership", "user" : "user-1"}) + assert vm1.status_code == 200 + assert vm1.json == {"status": "OK"} + +# Try to transfert the VM as an a non admin +def test_non_admin_transfer_ownership(client, init_user_database, init_vm_database, monkeypatch): + monkeypatch.setattr(util, "check_cas_token", fake_check_cas_token) + monkeypatch.setattr(proxmox, "get_node_from_vm", fake_get_node_from_vm) + monkeypatch.setattr(proxmox, "transfer_ownership", fake_transfer_ownership) + + vm1 = client.patch('/api/1.0.0/vm/1', headers={'Content-Type': 'application/json', "Authorization" : "Bearer AT-TEST"}, json={"status": "transfering_ownership", "user" : "user-1"}) + assert vm1.status_code == 403 + assert vm1.json == {"status": "Permission denied"} + # Try to patch with an invalid token def test_invalid_patch_vm(client, init_user_database, init_vm_database, monkeypatch): monkeypatch.setattr(util, "check_cas_token", fake_check_cas_token_fail) @@ -290,5 +315,4 @@ def test_admin_get_other_account_state(client, init_user_database, monkeypatch): assert user1.status_code == 200 assert user1.json == {"freeze_state": "state"} assert user2.status_code == 200 - assert user2.json == {"freeze_state": "state"} - \ No newline at end of file + assert user2.json == {"freeze_state": "state"} \ No newline at end of file diff --git a/backend/test/test_stop_expired_vm.py b/backend/test/test_stop_expired_vm.py new file mode 100644 index 0000000..d94eef3 --- /dev/null +++ b/backend/test/test_stop_expired_vm.py @@ -0,0 +1,30 @@ +import pytest +from proxmox_api import proxmox +from test.conftest import * + +stopped_func = [] + +# mocking func +def mock_stop_vm(vm_id, node): + stopped_func.append(vm_id) + return True + +def mock_get_node_from_vm(vmid): + return "node1",200 + +def mock_get_proxmox_vm_status(vmid, node): + if vmid == 5 : + return {"status" : "stopped"} ,200 + return {"status" : "running"}, 200 + + +def test_stop_expired_vm(init_user_database, init_expired_vm_database, monkeypatch): + monkeypatch.setattr(proxmox, "stop_vm", mock_stop_vm) + monkeypatch.setattr(proxmox, "get_node_from_vm", mock_get_node_from_vm) + monkeypatch.setattr(proxmox, "get_proxmox_vm_status", mock_get_proxmox_vm_status) + proxmox.stop_expired_vm(flask_app.app) + assert 3 in stopped_func + assert 4 in stopped_func + + + \ No newline at end of file diff --git a/backend/test/test_vm_creation_form.py b/backend/test/test_vm_creation_form.py index 14410b8..8fa22a3 100644 --- a/backend/test/test_vm_creation_form.py +++ b/backend/test/test_vm_creation_form.py @@ -2,34 +2,81 @@ from proxmox_api import proxmox from test.conftest import * from proxmox_api.db import db_functions +from proxmoxer import ProxmoxAPI +def fake_next_available_vmid(): + return "999" +def fake_set_new_vm_ip(next_vmid, node): + return "127.0.0.1" +def fake_load_balance_server(): + return {'server' :'wammu'},201 +def fake_proxmox_clone_vm(name, newid,target, full): + return True -def test_creation_for_new_user(monkeypatch,init_user_database, init_vm_database, proxmoxAPI): - """Test case for creation of VM by a new user - The creation does not concern proxmox - """ +def fake_vm_config(vmid, node, password, vm_usermain_ssh_key, ip): + return True +def fake_check_update_cotisation(user_id): + return {"freezeState": "1"}, 200 - node = "wammu" - def fake_next_available_vmid(): - return "999" - def fake_set_new_vm_ip(next_vmid, node): - return "127.0.0.1" +def fake_config_vm(next_vmid, node, password, vm_user, main_ssh_key,ip): + return True + +class _ProxmoxAPIRessources: + def __init__(self): + pass + + def get(*args, **kwargs): + return [{"vmid": "10003", "node":"kars"}] + +class _ProxmoxAPIcluster: + def __init__(self): + pass + + resources = _ProxmoxAPIRessources() + + +class _ProxmoxAPIClone: + def __init__(self): + pass + + def create(*args, **kwargs): + return True +class _ProxmoxAPIQemu: + clone = _ProxmoxAPIClone() + def __init__(self): + pass + + +class _ProxmoxAPINode: + def __init__(self): + pass + + def qemu(self, vmid): + return _ProxmoxAPIQemu() - def fake_load_balance_server(): - return {'server' :'wammu'},201 - def fake_proxmox_clone_vm(name, newid,target, full): - return True +class _ProxmoxAPI: + def __init__(self, node, monkeypatch): + pass + cluster = _ProxmoxAPIcluster() + + def nodes(self, node): + return _ProxmoxAPINode() + + - def fake_vm_config(vmid, node, password, vm_user,main_ssh_key, ip): - return True - def fake_check_update_cotisation(user_id): - return {"freezeState": "1"}, 200 + +def test_creation_for_new_user(monkeypatch,init_user_database, init_vm_database): + """Test case for creation of VM by a new user + The creation does not concern proxmox + """ + + node = "wammu" @@ -40,13 +87,25 @@ def fake_check_update_cotisation(user_id): # Mocking monkeypatch.setattr(proxmox, 'next_available_vmid', fake_next_available_vmid) monkeypatch.setattr(proxmox, 'set_new_vm_ip', fake_set_new_vm_ip) - monkeypatch.setattr(proxmoxAPI.nodes("wammu").qemu(10003).clone, 'create', fake_proxmox_clone_vm) + + """proxmoxapi = ProxmoxAPI(host=configuration.PROXMOX_HOST, user=configuration.PROXMOX_USER + , token_name=configuration.PROXMOX_API_KEY_NAME + , token_value=configuration.PROXMOX_API_KEY, verify_ssl=False) +""" + #monkeypatch.setattr(proxmox.proxmox.nodes("kars").qemu(10003).clone,'create', fake_proxmox_clone_vm) + node="kars" + realProxmox = proxmox.proxmox + proxmox.proxmox = _ProxmoxAPI(node, monkeypatch) + + #monkeypatch.setattr(proxmox.proxmox, "nodes", lambda self, node: _ProxmoxAPI(node, monkeypatch)) monkeypatch.setattr(proxmox, 'load_balance_server', fake_load_balance_server) monkeypatch.setattr(proxmox, 'config_vm', fake_vm_config) monkeypatch.setattr(proxmox, 'check_update_cotisation', fake_check_update_cotisation) + monkeypatch.setattr(proxmox, 'config_vm', fake_config_vm) userId = "new-user6" body,status = proxmox.create_vm("INTEGRATION-TEST-VM", "bare_vm", userId, password="1A#aaaaaaaaa", vm_user="user", main_ssh_key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKWkpOTUuLKpZEQT2CmEsgZwZzegitYCx/8vHICvv261 fake@key") + proxmox.proxmox = realProxmox assert status == 201 userVms = db_functions.get_vm_list(user_id=userId) assert len(userVms) == 1 @@ -55,7 +114,6 @@ def fake_check_update_cotisation(user_id): - diff --git a/backend/test/test_vm_ownership_transfer.py b/backend/test/test_vm_ownership_transfer.py new file mode 100644 index 0000000..e4596a1 --- /dev/null +++ b/backend/test/test_vm_ownership_transfer.py @@ -0,0 +1,82 @@ +import pytest +from test.conftest import * +from proxmox_api import proxmox +from proxmox_api import util +from proxmox_api.db import db_functions as database + + +def fake_get_adh6_account(newowner): + return {"username": newowner}, 200 + + +# Transfer a vm to another user, who exists in hosting and has less than 3 vms +def test_vm_ownership_transfer(monkeypatch, init_user_database, init_vm_database): + username = "user-2" + monkeypatch.setattr(util, 'get_adh6_account', fake_get_adh6_account) + vmid = 1 + body, status = proxmox.transfer_ownership(vmid, username) + + #return status + assert status == 201 + assert body == {"status": "ok"} + + # Check that the vm is now owned by user-2 + userid = database.get_vm_userid(vmid) + assert userid == username + + # Check that the history is updated + history = database.get_historyip_fromdb(vmid) + assert history[-1][2] == username + +# Transfer a vm to another user, who doesn't exist in hosting but exist in adh6 +def test_vm_ownership_transfer_to_new_user(monkeypatch, init_user_database, init_vm_database): + username = "new-user" + monkeypatch.setattr(util, 'get_adh6_account', fake_get_adh6_account) + vmid = 1 + body, status = proxmox.transfer_ownership(vmid, username) + + #return status + assert status == 201 + assert body == {"status": "ok"} + + # Check that the user is created in the database + assert database.get_user_list(user_id=username) is not None + + # Check that the vm is now owned by user-2 + userid = database.get_vm_userid(vmid) + assert userid == username + + # Check that the history is updated + history = database.get_historyip_fromdb(vmid) + assert history[-1][2] == username + +# Transfer a vm to another user, who doesn't exist in hosting not in adh6 +def test_vm_ownership_to_nonexistent_user(monkeypatch,init_user_database, init_vm_database): + username = "nonexistent-user" + def fake_get_adh6_account_None(newowner): + return None, 200 + + monkeypatch.setattr(util, 'get_adh6_account', fake_get_adh6_account_None) + vmid = 1 + body, status = proxmox.transfer_ownership(vmid, username) + + #return status + assert status == 404 + assert body == {"error": "User not found"} + +# Transfer a vm to another user, who has already 3 vms +def test_vm_ownership_to_full_user(monkeypatch, init_user_database, init_vm_database): + username = "user-1" # User-1 has 3 vms + monkeypatch.setattr(util, 'get_adh6_account', fake_get_adh6_account) + vmid = 3 # vmid 3 is owned by user-2 + body, status = proxmox.transfer_ownership(vmid, username) + + #return status + assert status == 400 + assert body == {"error": "User already has 3 VMs"} + + # We check that the vm is still owned by user-2 + # Check that the vm is now owned by user-2 + userid = database.get_vm_userid(vmid) + assert userid == "user-2" + diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 135aa6c..0865467 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -3,7 +3,7 @@
- +
+
+
+
+
+
+
+
+ Compte actuel : {{user.vms[0].user}}
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+