commit f3dd764963dbaf46f08664d3e73c93a6877a861f Author: Fabien Toune Date: Fri Jan 8 10:55:31 2021 +0100 initial_commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0ecc3ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,134 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +.vscode/ +*.sqlite3 +ovh.conf + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + diff --git a/ovh_.conf b/ovh_.conf new file mode 100644 index 0000000..684b7d0 --- /dev/null +++ b/ovh_.conf @@ -0,0 +1,7 @@ +[default] +endpoint=ovh-eu + +[ovh-eu] +application_key= +application_secret= +consumer_key= \ No newline at end of file diff --git a/ovh_management.py b/ovh_management.py new file mode 100644 index 0000000..47aa083 --- /dev/null +++ b/ovh_management.py @@ -0,0 +1,315 @@ +# -*- encoding: utf-8 -*- +import PySimpleGUI as sg +import ovh + +client = ovh.Client() +service = client.get('/cloud/project')[0] +service_url = f'/cloud/project/{service}' + +# regions = [f['region'] for f in flavors if f['name'] == 's1-2'] +regions = ['GRA7', 'SBG5', 'DE1', 'UK1', 'BHS5'] + + +def get_flavor_id(region, name='s1-2'): + flavors = client.get(service_url + '/flavor') + flavorId = [f['id'] for f in flavors if f['name'] == name and f['region']==region][0] + return flavorId + +def get_image_id(region, name='Ubuntu 20.04'): + images = client.get(service_url + '/image') + imageId = [i['id'] for i in images if i['name']==name and i['region']==region][0] + return imageId + +def get_eleves(): + ovh_ssh_keys = client.get(service_url + '/sshkey') + liste_eleves = [(k['id'], k['name'].split('_')[1], k['publicKey']) + for k in ovh_ssh_keys + if k['name'].startswith('ssh_')] + return liste_eleves + +def delete_eleve(eleve_id): + client.delete(service_url + '/sshkey/' + eleve_id) + +def create_eleve(prenom, publicKey): + client.post(service_url + '/sshkey', + name = 'ssh_' + prenom, + publicKey = publicKey + ) + +def get_instances(): + ovh_instances = client.get(service_url + '/instance') + instances = [] + for i in ovh_instances: + id_inst = i['id'] + name = i['name'] + if not name.startswith('eica-'): + continue + if i['ipAddresses']: + ip = i['ipAddresses'][0]['ip'] + else: + ip = 'Waiting...' + region = i['region'] + status = i['status'] + instances.append((id_inst, name, ip, region, status,)) + return instances + +def create_instance(eleve_id, name, region): + image_id = get_image_id(region) + flavor_id = get_flavor_id(region) + client.post(service_url + '/instance', + name = 'eica-' + name, + sshKeyId = eleve_id, + flavorId = flavor_id, + imageId = image_id, + region = region + ) + +def delete_instance(instance_id): + print(service_url + '/instance/' + instance_id) + client.delete(service_url + '/instance/' + instance_id) + +def make_main_window(): + liste_eleves = get_eleves() + liste_instances = get_instances() + col = [ + [sg.Button('Ajouter', size=(12, 1))], + [sg.Button('Modifier', size=(12, 1))], + [sg.Button('Supprimer', size=(12, 1))], + [sg.Combo(regions, size=(12, 1), default_value=regions[0], k='region')], + [sg.Button('Instancier', size=(12, 1), button_color=('white', 'green'))], + [sg.Button('Quitter', size=(12, 1))], + ] + col1 = [ + [sg.Table(values=liste_instances, + headings=['id', 'Nom', 'Adresse IP', 'Région', 'Statut'], + max_col_width=25, + col_widths=[0, 15, 12, 10, 10], + hide_vertical_scroll=True, + background_color='light blue', + text_color='black', + auto_size_columns=False, + justification='right', + num_rows=len(liste_instances), + alternating_row_color='lightyellow', + key='table_instances', + tooltip='Liste des instances actives')], + ] + col2 = [ + [sg.Button('Détruire', size=(12, 1), button_color=('white', 'red'))], + [sg.Button('Actualiser', size=(12, 1))], + ] + layout = [ + [sg.Text('Liste des élèves')], + [sg.Table(values=liste_eleves, + headings=['', 'Prénom', ''], + max_col_width=25, + col_widths=[0, 20, 0], + hide_vertical_scroll=True, + background_color='light blue', + text_color='black', + auto_size_columns=False, + justification='right', + num_rows=len(liste_eleves), + alternating_row_color='lightyellow', + key='table_eleves', + tooltip='Liste des élèves'), + sg.Column(col), + sg.Column(col1), + sg.Column(col2)], + + ] + window = sg.Window('OVH - Gestion des instances', + layout=layout, + size=(900, 300), + finalize=True) + window.liste_eleves = liste_eleves + window.liste_instances = liste_instances + return window + + +def make_add_window(caption, role='create', initial_values=None): + if initial_values: + eleve_id, prenom, pk = initial_values + else: + eleve_id = prenom = pk = '' + layout = [ + [sg.InputText(eleve_id, k='eleve_id', visible=True)], + [sg.Text(text='Prénom', size=(10, 2)), sg.InputText( + default_text=prenom, size=(20, 2), k='prenom')], + [sg.Text(text='Clé publique', size=(10, 2)), sg.InputText( + default_text=pk, size=(20, 2), k='pk')], + [sg.Button('Valider', bind_return_key=True), + sg.Button('Annuler', bind_return_key=True)] + ] + window = sg.Window(caption, + layout=layout, + finalize=True) + window.role = role + return window + + +window1, window2 = make_main_window(), None + +while True: + window, event, values = sg.read_all_windows() + print(f'window : {window}, event : {event}, values : {values}') + if window == window1: + if event in (sg.WIN_CLOSED, 'Quitter'): + break + + if event == 'Ajouter': + window2 = make_add_window('Ajouter', role='create') + window2.make_modal() + + if event == 'Supprimer': + try: + # dans le cas d'un tableau, la valeur est un tableau de(s) + # index sélectionnés. On garde le premier quoi qu'il arrive + selected_line = values['table_eleves'][0] + # la colonne id est cachée au niveau de l'affichage + id_eleve = window1.liste_eleves[selected_line][0] + except IndexError: + sg.popup_ok( + 'Veuillez sélectionner un enregistrement à supprimer') + continue + confirmation = sg.popup("Confirmez-vous la suppression ?", + button_type=sg.POPUP_BUTTONS_OK_CANCEL, + custom_text=("Oui", "Non")) + if confirmation == "Non": + continue + sg.PopupAnimated("sablier.png") + + delete_eleve(id_eleve) + liste_eleves = get_eleves() + + sg.PopupAnimated(None) + window1['table_eleves'].update(values=liste_eleves, + num_rows=len(liste_eleves)) + window1.liste_eleves = liste_eleves + + if event == 'Modifier': + try: + # voir 'Supprimer' pour une explication + selected_line = values['table_eleves'][0] + id_eleve, prenom, pk = window1.liste_eleves[selected_line] + except IndexError: + sg.popup_ok( + 'Veuillez sélectionner un enregistrement à modifier') + continue + window2 = make_add_window('Modifier', + initial_values=[id_eleve, prenom, pk], + role='modify') + window2.make_modal() + + if event == 'Instancier': + selected_lines = values['table_eleves'] + if len(selected_lines) == 0: + sg.popup_ok( + 'Veuillez sélectionner un élève') + continue + eleves = [] + for row in selected_lines: + id_eleve = window1.liste_eleves[row][0] + prenom = window1.liste_eleves[row][1] + eleves.append((id_eleve, prenom,)) + nb_inst = len(eleves) + confirmation = sg.popup(f"Confirmez-vous le lancement de {nb_inst} instances ?", + button_type=sg.POPUP_BUTTONS_OK_CANCEL, + custom_text=("Oui", "Non")) + if confirmation == "Non": + continue + + sg.PopupAnimated("sablier.png") + + for el in eleves: + active_instances_names = [i[1].split('-')[1] for i in window1.liste_instances] + if not el[1] in active_instances_names: + create_instance(el[0], el[1], values['region']) + + liste_instances = get_instances() + + sg.PopupAnimated(None) + + window1['table_instances'].update(values=liste_instances, + num_rows=len(liste_instances)) + window1.liste_instances = liste_instances + + if event == 'Détruire': + selected_lines = values['table_instances'] + if len(selected_lines) == 0: + sg.popup_ok( + 'Veuillez sélectionner une instance au moins') + continue + instances = [] + for row in selected_lines: + instances.append(window1.liste_instances[row][0]) + nb_inst = len(instances) + confirmation = sg.popup(f"Confirmez-vous la suppression de {nb_inst} instances ?", + button_type=sg.POPUP_BUTTONS_OK_CANCEL, + custom_text=("Oui", "Non")) + if confirmation == "Non": + continue + + sg.PopupAnimated("sablier.png") + + for id_inst in instances: + delete_instance(id_inst) + + + liste_instances = get_instances() + + sg.PopupAnimated(None) + + window1['table_instances'].update(values=liste_instances, + num_rows=len(liste_instances)) + window1.liste_instances = liste_instances + + + if event == 'Actualiser': + sg.PopupAnimated("sablier.png") + liste_instances = get_instances() + sg.PopupAnimated(None) + window1['table_instances'].update(values=liste_instances, + num_rows=len(liste_instances)) + window1.liste_instances = liste_instances + + + if window == window2: + if event in (sg.WIN_CLOSED, 'Annuler'): + window2.close() + window2 = None + + if event == 'Valider' and window.role == 'create': + prenom = values['prenom'].lower() # TODO : accents ? + pk = values['pk'] + create_eleve(prenom, pk) + liste_eleves = get_eleves() + window1['table_eleves'].update(values=liste_eleves, + num_rows=len(liste_eleves)) + window1.liste_eleves = liste_eleves + window2.close() + window2 = None + + if event == 'Valider' and window.role == 'modify': + confirmation = sg.popup("Confirmez-vous la modification ?", + button_type=sg.POPUP_BUTTONS_OK_CANCEL, + custom_text=("Oui", "Non")) + if confirmation == "Non": + continue + prenom = values['prenom'].lower() + pk = values['pk'] + eleve_id = values['eleve_id'] + delete_eleve(eleve_id) + create_eleve(prenom, pk) + liste_eleves = get_eleves() + window1['table_eleves'].update(values=liste_eleves, + num_rows=len(liste_eleves)) + window1.liste_eleves = liste_eleves + window2.close() + window2 = None + +window1.close() +if window2 is not None: + window2.close() + +print('Bye !') diff --git a/sablier.png b/sablier.png new file mode 100644 index 0000000..7361e8e Binary files /dev/null and b/sablier.png differ