Compare commits

...

11 Commits

Author SHA1 Message Date
Alejandro Sirgo Rica 4fe97139c5 cli: remove duplicate payload argument in get()
Use only one payload function argument in the http get() function.
2024-12-18 14:10:52 +01:00
Alejandro Sirgo Rica 7d2678422e cli: cleanup http error handling
Use more specific exception request exception handling to provide
better error messages.

Define an interal request function for get, post and delete to
reuse code.
2024-12-16 12:49:46 +01:00
Alejandro Sirgo Rica 2f870d7b6a cli: add lives alias to the live command argument 2024-12-13 13:59:54 +01:00
Alejandro Sirgo Rica 80a68ceb5a live: use human readable date in live list
Parse the unix timestamp into a human readable format like
"2024-11-25 12:28:19".

Make print_json() print from string and from a json object to
enable manipulation of data before printing.
2024-12-13 13:59:39 +01:00
Alejandro Sirgo Rica 12d965ce1c cli: improve HTTP status code error logging
Use a different message for each status code when the request is
not successful.
2024-12-10 11:28:44 +01:00
Alejandro Sirgo Rica 4b77e1bca8 folder: fix add folder command
Fix typo causing an error in the add folder command.
2024-12-10 10:45:31 +01:00
Alejandro Sirgo Rica 0225502832 cli: add live set --default command
Add command to set a new default live.

Command example:
ogcli set live --default --name ogLive-5.4.0-r20220408
2024-12-05 13:25:46 +01:00
Alejandro Sirgo Rica 85d910d020 cli: add live management commands
Add new parameters to the config file ogcli.json with the
following default values:
'local_live': '/var/www/html/ogrelive'
'server_live': 'https://opengnsys.soleta.eu/ogrelive'

Add command to install the files of a live system. Example:
ogcli install live --name ogrelive-6.1.0-26

Perform an update if live files are already present.

Add command to delete the files of a live system. Example:
ogcli delete live --name ogrelive-6.1.0-26

Update ogcli list live to show the lives in the server when
invoked with the --remote flag. Example:
ogcli list live --remote
2024-11-21 17:05:26 +01:00
Alejandro Sirgo Rica c4eb5d165a ogcli: move configuration data into a different file
Move the configuration into its own file so it is easier to access
from multiple files.
2024-11-21 17:05:26 +01:00
Alejandro Sirgo Rica 10a3897f92 cli: ensure the program returns 0 on success and 1 on error
propagate a returncode in each operation and make it the
returncode of the program.

Prevent sys.exit calls in post(), get() and delete() request
handlers to enable cleanup code and error handling. Keep a basic
error log inside the request functions if the connection can't
be established or if the response contains an error code.
2024-11-21 17:05:24 +01:00
OpenGnSys Support Team ea8210a805 Revert "cli: rename ogcli list scopes to ogcli list scope"
This reverts commit 8f5b709212.
2024-11-21 16:44:59 +01:00
20 changed files with 743 additions and 236 deletions

View File

@ -143,7 +143,7 @@ ogcli setup disk --type dos --part 1,LINUX,EXT4,40G --part 4,CACHE,CACHE,10G --f
##### Fetching a classroom id
```
ogcli list scope
ogcli list scopes
{'scope': [{'name': 'Unidad Organizativa (Default)', 'type': 'center', 'id': 1, 'scope': [{'name': 'Aula virtual', 'type': 'room', 'id': 1, ...
```

View File

@ -20,109 +20,129 @@ from cli.objects.center import OgCenter
from cli.objects.room import OgRoom
from cli.objects.folder import OgFolder
from cli.objects.session import OgSession
from cli.config import cfg
import argparse
import requests
import sys
def _log_http_status_code(res):
if res.status_code == 400:
print('Error 400: invalid payload')
elif res.status_code == 404:
print('Error 404: object not found')
elif res.status_code == 405:
print('Error 405: method not allowed')
elif res.status_code == 409:
print('Error 409: object already exists')
elif res.status_code == 423:
print('Error 423: object in use')
elif res.status_code == 501:
print('Error 501: cannot connect to database')
elif res.status_code == 507:
print('Error 500: disk full')
else:
print(f'Received status code {res.status_code}')
class OgREST():
def __init__(self, ip, port, api_token):
if not ip or not port or not api_token:
raise ValueError("IP, port, and API token must be provided.")
self.URL = f'http://{ip}:{port}'
self.HEADERS = {'Authorization': api_token}
def get(self, path, payload=None):
def _request(self, method, path, payload, expected_status):
try:
r = requests.get(f'{self.URL}{path}',
headers=self.HEADERS,
json=payload)
if r.status_code != 200:
sys.exit(f"Unsuccessful request to ogServer: "
f"Response with {r.status_code} HTTP status code")
except IOError as e:
sys.exit(f"Cannot connect to ogServer: {e}")
return r
res = requests.request(
method,
f'{self.URL}{path}',
headers=self.HEADERS,
json=payload,
)
if res.status_code not in expected_status:
_log_http_status_code(res)
return None
return res
except requests.exceptions.ConnectionError:
print("Cannot connect to ogserver")
except requests.exceptions.Timeout:
print("Request to ogserver timed out")
except requests.exceptions.TooManyRedirects:
print("Too many redirects occurred while contacting ogserver")
except requests.exceptions.RequestException as e:
print(f"An error occurred while contacting ogserver: {e}")
return None
def get(self, path, payload=None):
return self._request('GET', path, payload, expected_status={200})
def post(self, path, payload):
try:
r = requests.post(f'{self.URL}{path}',
headers=self.HEADERS,
json=payload)
if r.text:
print(r.text)
if r.status_code not in {200, 202}:
sys.exit(f"Unsuccessful request to ogServer: "
f"Response with {r.status_code} HTTP status code")
except IOError as e:
sys.exit(f"Cannot connect to ogServer: {e}")
return r
return self._request('POST', path, payload, expected_status={200, 202})
def delete(self, path, payload):
try:
r = requests.delete(f'{self.URL}{path}',
headers=self.HEADERS,
json=payload)
print(r.text)
if r.status_code != 200:
sys.exit(f"Unsuccessful request to ogServer: "
f"Response with {r.status_code} HTTP status code")
except IOError as e:
sys.exit(f"Cannot connect to ogServer: {e}")
return r
return self._request('DELETE', path, payload, expected_status={200})
class OgCLI():
def __init__(self, cfg):
def __init__(self):
self.rest = OgREST(cfg['ip'], cfg['port'], cfg['api_token'])
def list(self, args):
choices = ['clients', 'scope', 'modes', 'hardware',
'client', 'images', 'disks', 'servers', 'repos', 'live']
choices = ['clients', 'scopes', 'modes', 'hardware',
'client', 'images', 'disks', 'servers', 'repos',
'live', 'lives']
parser = argparse.ArgumentParser(prog='ogcli list')
parser.add_argument('item', choices=choices)
if not args:
print('Missing list subcommand', file=sys.stderr)
parser.print_help(file=sys.stderr)
sys.exit(1)
return 1
parsed_args = parser.parse_args([args[0]])
ret = 0
if parsed_args.item == 'clients':
OgClient.list_clients(self.rest)
ret = OgClient.list_clients(self.rest)
elif parsed_args.item == 'client':
OgClient.get_client_properties(self.rest, args[1:])
ret = OgClient.get_client_properties(self.rest, args[1:])
elif parsed_args.item == 'hardware':
OgClient.list_client_hardware(self.rest, args[1:])
ret = OgClient.list_client_hardware(self.rest, args[1:])
elif parsed_args.item == 'modes':
OgModes.list_available_modes(self.rest)
elif parsed_args.item == 'scope':
OgScope.list_scopes(self.rest, args[1:])
ret = OgModes.list_available_modes(self.rest)
elif parsed_args.item == 'scopes':
ret = OgScope.list_scopes(self.rest, args[1:])
elif parsed_args.item == 'images':
OgImage.list_images(self.rest)
ret = OgImage.list_images(self.rest)
elif parsed_args.item == 'disks':
OgDisk.list_disks(self.rest, args[1:])
ret = OgDisk.list_disks(self.rest, args[1:])
elif parsed_args.item == 'repos':
OgRepo.list_repos(self.rest)
ret = OgRepo.list_repos(self.rest)
elif parsed_args.item == 'servers':
OgServer.list_servers(self.rest)
elif parsed_args.item == 'live':
OgLive.list_live(self.rest)
ret = OgServer.list_servers(self.rest)
elif parsed_args.item in ['live', 'lives']:
ret = OgLive.list_live(self.rest, args[1:])
return ret
def set(self, args):
choices = ['modes', 'mode', 'repo']
choices = ['modes', 'mode', 'repo', 'live', 'lives']
parser = argparse.ArgumentParser(prog='ogcli set')
parser.add_argument('item', choices=choices)
if not args:
print('Missing set subcommand', file=sys.stderr)
parser.print_help(file=sys.stderr)
sys.exit(1)
return 1
parsed_args = parser.parse_args([args[0]])
ret = 0
if parsed_args.item in ['modes', 'mode']:
OgModes.set_modes(self.rest, args[1:])
ret = OgModes.set_modes(self.rest, args[1:])
elif parsed_args.item == 'repo':
OgRepo.set_repo(self.rest, args[1:])
ret = OgRepo.set_repo(self.rest, args[1:])
elif parsed_args.item in ['live', 'lives']:
ret = OgLive.set_live(self.rest, args[1:])
return ret
def request(self, args):
choices = ['reboot', 'refresh', 'poweroff', 'wol', 'session']
@ -132,19 +152,21 @@ class OgCLI():
if not args:
print('Missing request subcommand', file=sys.stderr)
parser.print_help(file=sys.stderr)
sys.exit(1)
return 1
parsed_args = parser.parse_args([args[0]])
ret = 0
if parsed_args.request_obj == 'wol':
OgWol.request_wol(self.rest, args[1:])
ret = OgWol.request_wol(self.rest, args[1:])
elif parsed_args.request_obj == 'poweroff':
OgPoweroff.request_poweroff(self.rest, args[1:])
ret = OgPoweroff.request_poweroff(self.rest, args[1:])
elif parsed_args.request_obj == 'refresh':
OgClient.request_refresh(self.rest, args[1:])
ret = OgClient.request_refresh(self.rest, args[1:])
elif parsed_args.request_obj == 'reboot':
OgReboot.request_reboot(self.rest, args[1:])
ret = OgReboot.request_reboot(self.rest, args[1:])
elif parsed_args.request_obj == 'session':
OgSession.request_session(self.rest, args[1:])
ret = OgSession.request_session(self.rest, args[1:])
return ret
def restore(self, args):
choices = ['image']
@ -154,11 +176,13 @@ class OgCLI():
if not args:
print('Missing restore subcommand', file=sys.stderr)
parser.print_help(file=sys.stderr)
sys.exit(1)
return 1
parsed_args = parser.parse_args([args[0]])
ret = 0
if parsed_args.restore_obj == 'image':
OgImage.restore_image(self.rest, args[1:])
ret = OgImage.restore_image(self.rest, args[1:])
return ret
def create(self, args):
choices = ['image']
@ -168,11 +192,13 @@ class OgCLI():
if not args:
print('Missing create subcommand', file=sys.stderr)
parser.print_help(file=sys.stderr)
sys.exit(1)
return 1
parsed_args = parser.parse_args([args[0]])
ret = 0
if parsed_args.create_obj == 'image':
OgImage.create_image(self.rest, args[1:])
ret = OgImage.create_image(self.rest, args[1:])
return ret
def setup(self, args):
choices = ['disk']
@ -182,11 +208,13 @@ class OgCLI():
if not args:
print('Missing setup subcommand', file=sys.stderr)
parser.print_help(file=sys.stderr)
sys.exit(1)
return 1
parsed_args = parser.parse_args([args[0]])
ret = 0
if parsed_args.setup_obj == 'disk':
OgDisk.setup_disk(self.rest, args[1:])
ret = OgDisk.setup_disk(self.rest, args[1:])
return ret
def update(self, args):
choices = ['image', 'repo', 'center', 'room', 'folder']
@ -196,43 +224,49 @@ class OgCLI():
if not args:
print('Missing update subcommand', file=sys.stderr)
parser.print_help(file=sys.stderr)
sys.exit(1)
return 1
parsed_args = parser.parse_args([args[0]])
ret = 0
if parsed_args.update_obj == 'image':
OgImage.update_image(self.rest, args[1:])
ret = OgImage.update_image(self.rest, args[1:])
elif parsed_args.update_obj == 'center':
OgCenter.update_center(self.rest, args[1:])
ret = OgCenter.update_center(self.rest, args[1:])
elif parsed_args.update_obj == 'room':
OgRoom.update_room(self.rest, args[1:])
ret = OgRoom.update_room(self.rest, args[1:])
elif parsed_args.update_obj == 'folder':
OgFolder.update_folder(self.rest, args[1:])
ret = OgFolder.update_folder(self.rest, args[1:])
elif parsed_args.update_obj == 'repo':
OgRepo.update_repo(self.rest, args[1:])
ret = OgRepo.update_repo(self.rest, args[1:])
return ret
def delete(self, args):
choices = ['server', 'repo', 'center', 'room', 'client', 'folder']
choices = ['server', 'repo', 'center', 'room', 'client', 'folder',
'live', 'lives']
parser = argparse.ArgumentParser(prog='ogcli delete')
parser.add_argument('delete_obj', choices=choices)
if not args:
print('Missing delete subcommand', file=sys.stderr)
parser.print_help(file=sys.stderr)
sys.exit(1)
return 1
parsed_args = parser.parse_args([args[0]])
if parsed_args.delete_obj == 'server':
OgServer.delete_server(self.rest, args[1:])
ret = OgServer.delete_server(self.rest, args[1:])
elif parsed_args.delete_obj == 'repo':
OgRepo.delete_repo(self.rest, args[1:])
ret = OgRepo.delete_repo(self.rest, args[1:])
elif parsed_args.delete_obj == 'center':
OgCenter.delete_center(self.rest, args[1:])
ret = OgCenter.delete_center(self.rest, args[1:])
elif parsed_args.delete_obj == 'room':
OgRoom.delete_room(self.rest, args[1:])
ret = OgRoom.delete_room(self.rest, args[1:])
elif parsed_args.delete_obj == 'client':
OgClient.delete_client(self.rest, args[1:])
ret = OgClient.delete_client(self.rest, args[1:])
elif parsed_args.delete_obj == 'folder':
OgFolder.delete_folder(self.rest, args[1:])
ret = OgFolder.delete_folder(self.rest, args[1:])
elif parsed_args.delete_obj in ['live', 'lives']:
ret = OgLive.delete_live(self.rest, args[1:])
return ret
def add(self, args):
choices = ['server', 'repo', 'center', 'room', 'client', 'folder']
@ -242,18 +276,36 @@ class OgCLI():
if not args:
print('Missing add subcommand', file=sys.stderr)
parser.print_help(file=sys.stderr)
sys.exit(1)
return 1
parsed_args = parser.parse_args([args[0]])
ret = 0
if parsed_args.add_obj == 'server':
OgServer.add_server(self.rest, args[1:])
ret = OgServer.add_server(self.rest, args[1:])
elif parsed_args.add_obj == 'repo':
OgRepo.add_repo(self.rest, args[1:])
ret = OgRepo.add_repo(self.rest, args[1:])
elif parsed_args.add_obj == 'center':
OgCenter.add_center(self.rest, args[1:])
ret = OgCenter.add_center(self.rest, args[1:])
elif parsed_args.add_obj == 'room':
OgRoom.add_room(self.rest, args[1:])
ret = OgRoom.add_room(self.rest, args[1:])
elif parsed_args.add_obj == 'client':
OgClient.add_client(self.rest, args[1:])
ret = OgClient.add_client(self.rest, args[1:])
elif parsed_args.add_obj == 'folder':
OgFolder.add_folder(self.rest, args[1:])
ret = OgFolder.add_folder(self.rest, args[1:])
return ret
def install(self, args):
choices = ['live', 'lives']
parser = argparse.ArgumentParser(prog='ogcli install')
parser.add_argument('install_obj', choices=choices)
if not args:
print('Missing install subcommand', file=sys.stderr)
parser.print_help(file=sys.stderr)
return 1
parsed_args = parser.parse_args([args[0]])
ret = 0
if parsed_args.install_obj in ['live', 'lives']:
ret = OgLive.install_live(self.rest, args[1:])
return ret

31
cli/config.py 100644
View File

@ -0,0 +1,31 @@
#!/usr/bin/env python3
# Copyright (C) 2020-2024 Soleta Networks <opengnsys@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
# Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
import json
OG_CLI_CFG_PATH = "/opt/opengnsys/etc/ogcli.json"
def load_config():
data = None
try:
with open(OG_CLI_CFG_PATH, 'r') as json_file:
data = json.load(json_file)
except json.JSONDecodeError:
sys.exit(f'ERROR: Failed parse malformed JSON file {OG_CLI_CFG_PATH}')
except Exception as e:
sys.exit(f'ERROR: cannot open {OG_CLI_CFG_PATH}: {e}')
required_cfg_params = {'api_token', 'ip', 'port'}
difference_cfg_params = required_cfg_params - data.keys()
if len(difference_cfg_params) > 0:
sys.exit(f'Missing {difference_cfg_params} key in '
f'json config file')
return data
cfg = load_config()

View File

@ -23,7 +23,10 @@ class OgCenter():
payload = {'name': parsed_args.name}
if parsed_args.desc:
payload['comment'] = parsed_args.desc
rest.post('/center/add', payload=payload)
res = rest.post('/center/add', payload=payload)
if not res:
return 1
return 0
@staticmethod
def update_center(rest, args):
@ -48,7 +51,10 @@ class OgCenter():
}
if parsed_args.comment:
payload['comment'] = parsed_args.comment
rest.post('/center/update', payload=payload)
res = rest.post('/center/update', payload=payload)
if not res:
return 1
return 0
@staticmethod
def delete_center(rest, args):
@ -60,4 +66,7 @@ class OgCenter():
help='center id in database')
parsed_args = parser.parse_args(args)
payload = {'id': parsed_args.id}
rest.post('/center/delete', payload=payload)
res = rest.post('/center/delete', payload=payload)
if not res:
return 1
return 0

View File

@ -14,8 +14,11 @@ class OgClient():
@staticmethod
def list_clients(rest):
r = rest.get('/clients')
print_json(r.text)
res = rest.get('/clients')
if not res:
return 1
print_json(res.text)
return 0
@staticmethod
def list_client_hardware(rest, args):
@ -28,8 +31,11 @@ class OgClient():
parsed_args = parser.parse_args(args)
payload = {'client': parsed_args.client_ip}
r = rest.get('/hardware', payload=payload)
print_json(r.text)
res = rest.get('/hardware', payload=payload)
if not res:
return 1
print_json(res.text)
return 0
@staticmethod
def get_client_properties(rest, args):
@ -41,8 +47,11 @@ class OgClient():
parsed_args = parser.parse_args(args)
payload = {'client': parsed_args.client_ip}
r = rest.get('/client/info', payload=payload)
print_json(r.text)
res = rest.get('/client/info', payload=payload)
if not res:
return 1
print_json(res.text)
return 0
@staticmethod
def request_refresh(rest, args):
@ -55,7 +64,10 @@ class OgClient():
parsed_args = parser.parse_args(args)
payload = {'clients': parsed_args.client_ip}
rest.post('/refresh', payload=payload)
res = rest.post('/refresh', payload=payload)
if not res:
return 1
return 0
@staticmethod
def add_client(rest, args):
@ -82,8 +94,8 @@ class OgClient():
nargs='?',
required=True,
help='specify the ip address of the client')
r = rest.get('/mode')
boot_choices = r.json()['modes']
res = rest.get('/mode')
boot_choices = res.json()['modes']
parser.add_argument('--boot-mode',
nargs='?',
required=True,
@ -100,7 +112,7 @@ class OgClient():
err = True
if err:
parser.print_help(file=sys.stderr)
sys.exit(1)
return 1
payload = {
'name': parsed_args.hostname,
'netmask': '0',
@ -113,7 +125,10 @@ class OgClient():
'netdriver': 'generic',
'livedir': 'ogLive'
}
rest.post('/client/add', payload=payload)
res = rest.post('/client/add', payload=payload)
if not res:
return 1
return 0
@staticmethod
def delete_client(rest, args):
@ -127,4 +142,7 @@ class OgClient():
payload = {
'clients': parsed_args.ip,
}
rest.post('/client/delete', payload=payload)
res = rest.post('/client/delete', payload=payload)
if not res:
return 1
return 0

View File

@ -23,8 +23,11 @@ class OgDisk():
parsed_args = parser.parse_args(args)
payload = {'client': [parsed_args.client_ip]}
r = rest.get('/client/setup', payload=payload)
print_json(r.text)
res = rest.get('/client/setup', payload=payload)
if not res:
return 1
print_json(res.text)
return 0
@staticmethod
def setup_disk(rest, args):
@ -79,8 +82,8 @@ class OgDisk():
help='Specific client IP')
parsed_args = parser.parse_args(args)
r = rest.get('/scopes')
scopes = r.json()
res = rest.get('/scopes')
scopes = res.json()
ips = set()
for center in parsed_args.center_id:
@ -93,11 +96,11 @@ class OgDisk():
ips.add(l)
if not ips:
print("Missing --client-ip, or --room-id/--center-id. No clients provided.")
return None
return 1
if not parsed_args.num.isdigit():
print(f'Invalid disk number: must be an integer value')
return
return 1
payload = {'clients': parsed_args.client_ip, 'type': disk_type_map[parsed_args.type], 'disk': str(parsed_args.num),
'cache': '0', 'cache_size': '0', 'partition_setup': []}
@ -109,31 +112,31 @@ class OgDisk():
if len(p) != 4:
print(f'Invalid partition: requires "num,part_scheme,fs,size", "{",".join(p)}" provided')
return
return 1
part_num, code, fs, size = p[0], p[1].upper(), p[2].upper(), p[3]
if not part_num.isdigit():
print(f'Invalid partition: the first parameter must be a number, "{part_num}" provided')
return
return 1
if part_num in defined_part_indices:
print(f'Invalid partition: partition number "{part_num}" has multiple definitions')
return
return 1
defined_part_indices.add(part_num)
if code not in part_types:
print(f'Invalid partition {i}: specified partition type {code} is not supported. The supported formats are {part_types}')
return
return 1
if fs not in fs_types:
print(f'Invalid partition {i}: specified filesystem {fs} is not supported. The supported formats are {fs_types}')
return
return 1
try:
size = parse_size(size)
except ValueError as error:
print(f'Invalid partition {i}: {str(error)}')
return
return 1
for j in range(i, int(part_num)):
part = {'partition': str(j), 'code': 'EMPTY',
@ -158,4 +161,7 @@ class OgDisk():
'format': '0'}
payload['partition_setup'].append(part)
rest.post('/setup', payload=payload)
res = rest.post('/setup', payload=payload)
if not res:
return 1
return 0

View File

@ -39,7 +39,10 @@ class OgFolder():
payload['room'] = parsed_args.room_id
if parsed_args.center_id:
payload['center'] = parsed_args.center_id
rest.post('/folder/add', payload=payload)
res = rest.post('/folder/add', payload=payload)
if not res:
return 1
return 0
@staticmethod
def update_folder(rest, args):
@ -58,7 +61,10 @@ class OgFolder():
'id': parsed_args.id,
'name': parsed_args.name,
}
rest.post('/folder/update', payload=payload)
res = rest.post('/folder/update', payload=payload)
if not res:
return 1
return 0
@staticmethod
def delete_folder(rest, args):
@ -70,4 +76,7 @@ class OgFolder():
help='folder id in database')
parsed_args = parser.parse_args(args)
payload = {'id': parsed_args.id}
rest.post('/folder/delete', payload=payload)
res = rest.post('/folder/delete', payload=payload)
if not res:
return 1
return 0

View File

@ -21,10 +21,10 @@ def get_repository(repository_id, rest):
def get_repositories(rest):
r = rest.get('/repositories')
if not r or r.status_code != requests.codes.ok:
res = rest.get('/repositories')
if not res:
return None
repositories = r.json()['repositories']
repositories = res.json()['repositories']
return repositories
@ -32,8 +32,11 @@ class OgImage():
@staticmethod
def list_images(rest):
r = rest.get('/images')
print_json(r.text)
res = rest.get('/images')
if not res:
return 1
print_json(res.text)
return 0
@staticmethod
def restore_image(rest, args):
@ -78,8 +81,8 @@ class OgImage():
help='Specific client IP')
parsed_args = parser.parse_args(args)
r = rest.get('/scopes')
scopes = r.json()
res = rest.get('/scopes')
scopes = res.json()
ips = set()
for center in parsed_args.center_id:
@ -93,34 +96,37 @@ class OgImage():
if not ips:
print('Missing --client-ip, or --room-id/--center-id. No clients provided.')
return
return 1
r = rest.get('/images')
images = r.json()
res = rest.get('/images')
images = res.json()
found_image = [img for img in images['images']
if img['id'] == parsed_args.id]
if not found_image:
print(f'Image with id {parsed_args.id} not found.')
return
return 1
else:
found_image = found_image[0]
selected_repo_id = 0
for ip in parsed_args.client_ip:
r = rest.get('/client/info', payload={'client': ip})
repo_id = r.json()['repo_id']
res = rest.get('/client/info', payload={'client': ip})
repo_id = res.json()['repo_id']
if selected_repo_id == 0:
selected_repo_id = repo_id
elif selected_repo_id != repo_id:
print(f'cannot restore clients assigned to different repositories')
return
return 1
payload = {'disk': parsed_args.disk, 'partition': parsed_args.part,
'id': str(parsed_args.id), 'name': found_image['name'],
'profile': str(found_image['software_id']),
'repository_id': repo_id,
'type': parsed_args.type.upper(), 'clients': list(ips)}
r = rest.post('/image/restore', payload=payload)
res = rest.post('/image/restore', payload=payload)
if not res:
return 1
return 0
@staticmethod
def create_image(rest, args):
@ -149,19 +155,19 @@ class OgImage():
help='Specific client IP')
parsed_args = parser.parse_args(args)
r = rest.get('/client/info', payload={'client': parsed_args.client_ip})
center_id = r.json()['center']
repo_id = r.json()['repo_id']
res = rest.get('/client/info', payload={'client': parsed_args.client_ip})
center_id = res.json()['center']
repo_id = res.json()['repo_id']
r = rest.get('/client/setup',
res = rest.get('/client/setup',
payload={'client': parsed_args.client_ip})
if r.status_code == 200:
if res.status_code == 200:
part_info = list(filter(lambda x: x['disk'] == int(parsed_args.disk) and
x['partition'] == int(parsed_args.part),
r.json()['partitions']))
res.json()['partitions']))
if not part_info:
print('Partition not found.')
return
return 1
fs_code = list(part_info)[0]['code']
image_name = remove_accents(parsed_args.name)
@ -171,7 +177,10 @@ class OgImage():
if parsed_args.desc:
payload['description'] = parsed_args.desc
rest.post('/image/create', payload=payload)
res = rest.post('/image/create', payload=payload)
if not res:
return 1
return 0
@staticmethod
def update_image(rest, args):
@ -202,32 +211,32 @@ class OgImage():
help='Specific client IP')
parsed_args = parser.parse_args(args)
r = rest.get('/client/info', payload={'client': parsed_args.client_ip})
center_id = r.json()['center']
repo_id = int(r.json()['repo_id'])
res = rest.get('/client/info', payload={'client': parsed_args.client_ip})
center_id = res.json()['center']
repo_id = int(res.json()['repo_id'])
r = rest.get('/client/setup',
res = rest.get('/client/setup',
payload={'client': parsed_args.client_ip})
if r.status_code == 200:
if res.status_code == 200:
part_info = list(filter(lambda x: x['disk'] == int(parsed_args.disk) and
x['partition'] == int(parsed_args.part),
r.json()['partitions']))
res.json()['partitions']))
if not part_info:
print('Partition not found.')
return
return 1
fs_code = list(part_info)[0]['code']
r = rest.get('/images')
res = rest.get('/images')
image_name = None
if r.status_code == 200:
for image in r.json()['images']:
if res.status_code == 200:
for image in res.json()['images']:
if image['id'] == int(parsed_args.id):
image_name = image['name']
break
if not image_name:
print('Image not found')
return
return 1
else:
print(f'Updating {image_name} image')
@ -241,4 +250,7 @@ class OgImage():
'code': str(fs_code),
'name': image_name}
rest.post('/image/update', payload=payload)
res = rest.post('/image/update', payload=payload)
if not res:
return 1
return 0

View File

@ -7,11 +7,317 @@
import argparse
from cli.config import cfg, OG_CLI_CFG_PATH
from datetime import datetime
from cli.utils import *
import requests
import shutil
import sys
import os
class OgLive():
live_files = [
'initrd.img',
'vmlinuz',
'filesystem.squashfs',
]
tmp_extension = '.tmp'
@staticmethod
def list_live(rest):
r = rest.get('/oglive/list')
print_json(r.text)
def _parse_timestamps(payload):
for elem in payload['oglive']:
if 'date' in elem:
timestamp = elem['date']
readable_date = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
elem['date'] = readable_date
@staticmethod
def _get_local_live_dir():
local_live_dir = cfg.get('local_live', '/var/www/html/ogrelive')
if not local_live_dir:
print(f'Error: local_live not defined in {OG_CLI_CFG_PATH}')
return None
if not os.path.isdir(local_live_dir):
print(f'Warning: {local_live_dir} directoy does not exist, creating directory')
try:
os.makedirs(local_live_dir, exist_ok=True)
except OSError as e:
print(f'ERROR: Failed to create directory {local_live_dir}: {e}')
return None
return local_live_dir
@staticmethod
def _get_server_base_url():
server_live = cfg.get('server_live', 'https://opengnsys.soleta.eu/ogrelive')
if not server_live:
print(f'Error: server_live not defined in {OG_CLI_CFG_PATH}')
return None
return server_live
def _is_live_in_server(live_name):
server_live = OgLive._get_server_base_url()
target_url = f'{server_live}/{live_name}/{OgLive.live_files[0]}'
response = requests.head(target_url)
return response.status_code == 200
@staticmethod
def _delete_tmp_live_files(live_name):
local_live_dir = OgLive._get_local_live_dir()
if not local_live_dir:
return 1
folder_path = os.path.join(local_live_dir, live_name)
for file_name in os.listdir(folder_path):
if not file_name.endswith(OgLive.tmp_extension):
continue
target_file = os.path.join(folder_path, file_name)
try:
os.remove(target_file)
except OSError as e:
print(f'ERROR: Failed to delete temporary file {target_file}: {e}')
return 1
return 0
@staticmethod
def _download_from_server(file_path, local_extension):
live_file = os.path.basename(file_path)
server_live = OgLive._get_server_base_url()
if not server_live:
return 1
file_url = f'{server_live}/{file_path}'
local_live_dir = OgLive._get_local_live_dir()
if not local_live_dir:
return 1
local_path = os.path.join(local_live_dir, file_path)
if local_extension:
local_path += local_extension
try:
response = requests.get(file_url, stream=True, timeout=(5, None))
except requests.exceptions.RequestException as e:
print(f'Request failed for {file_url}: {e}')
return 1
if response.status_code == 200:
try:
with open(local_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
except OSError as e:
print(f'File system error occurred: {e}')
return 1
else:
print(f'ERROR: Failed to download {live_file}. Status code: {response.status_code}')
return 1
return 0
@staticmethod
def list_live(rest, args):
parser = argparse.ArgumentParser(prog='ogcli list live')
parser.add_argument('--remote',
action='store_true',
help='(Optional) Obtain the list of the remote instead of the local lives')
parsed_args = parser.parse_args(args)
if parsed_args.remote:
local_live_dir = OgLive._get_local_live_dir()
if not local_live_dir:
return 1
download_err = OgLive._download_from_server('ogrelive.json',
local_extension=OgLive.tmp_extension)
if download_err:
OgLive._delete_tmp_live_files('')
return 1
remote_json = os.path.join(local_live_dir, 'ogrelive.json')
remote_json_tmp = remote_json + OgLive.tmp_extension
try:
shutil.move(remote_json_tmp, remote_json)
except OSError as e:
print(f'ERROR: cannot move {remote_json_tmp} into {remote_json}: {e}')
return 1
try:
with open(remote_json, 'r') as json_file:
remote_data = json_file.read()
except json.JSONDecodeError:
print(f'ERROR: Failed parse malformed JSON file {remote_json}')
return 1
except OSError as e:
print(f'ERROR: cannot open {remote_json}: {e}')
return 1
print_json(remote_data)
return 0
res = rest.get('/oglive/list')
if not res:
return 1
live_data = json.loads(res.text)
OgLive._parse_timestamps(live_data)
print_json(live_data)
return 0
@staticmethod
def _is_same_checksum(file_path, checksum_path):
local_checksum = compute_md5(file_path)
try:
with open(checksum_path, 'r') as f:
remote_checksum = f.read().strip()
except (FileNotFoundError, PermissionError, OSError) as e:
print(f'ERROR: Cannot read checksum file for {live_file}: {e}')
return False
return local_checksum == remote_checksum
@staticmethod
def install_live(rest, args):
parser = argparse.ArgumentParser(prog='ogcli install live')
parser.add_argument('--name',
nargs='?',
required=True,
help='Name of the center')
parsed_args = parser.parse_args(args)
live_name = parsed_args.name
local_live_dir = OgLive._get_local_live_dir()
if not local_live_dir:
return 1
if not OgLive._is_live_in_server(live_name):
print(f'{live_name} is not available on the server, it cannot be installed')
return 1
local_dir = os.path.join(local_live_dir, live_name)
if os.path.exists(local_dir):
print(f'{live_name} already exists, checking for updates...')
try:
os.makedirs(local_dir, exist_ok=True)
except OSError as e:
print(f'ERROR: Failed to create directory {local_dir}: {e}')
return 1
for live_file in OgLive.live_files:
download_err = OgLive._download_from_server(os.path.join(live_name,
live_file + '.full.sum'),
local_extension=OgLive.tmp_extension)
if download_err:
OgLive._delete_tmp_live_files(live_name)
return download_err
file_path = os.path.join(local_dir, live_file)
file_path_tmp = file_path + OgLive.tmp_extension
checksum_path_tmp = file_path + '.full.sum' + OgLive.tmp_extension
is_first_install = not os.path.exists(file_path)
if is_first_install:
print(f'Downloading {live_file}...')
else:
requires_update = not OgLive._is_same_checksum(file_path, checksum_path_tmp)
if not requires_update:
print(f'{live_file} is up-to-date, skipping')
continue
print(f'Updating {live_file}...')
download_err = OgLive._download_from_server(os.path.join(live_name,
live_file),
local_extension=OgLive.tmp_extension)
if download_err:
OgLive._delete_tmp_live_files(live_name)
return download_err
if not OgLive._is_same_checksum(file_path_tmp, checksum_path_tmp):
print(f'ERROR: Checksum mismatch for {live_file}')
OgLive._delete_tmp_live_files(live_name)
return 1
print(f'Checksum is OK for {live_file}')
for file_name in os.listdir(local_dir):
if not file_name.endswith(OgLive.tmp_extension):
continue
file_path_tmp = os.path.join(local_dir, file_name)
file_path = file_path_tmp[:-len(OgLive.tmp_extension)]
try:
shutil.move(file_path_tmp, file_path)
except OSError as e:
print(f'ERROR: cannot move {src_file} into {target_file}: {e}')
OgLive._delete_tmp_live_files(live_name)
return 1
payload = {'name': live_name}
res = rest.post('/oglive/add', payload=payload)
if not res:
return 1
return 0
@staticmethod
def delete_live(rest, args):
parser = argparse.ArgumentParser(prog='ogcli delete live')
parser.add_argument('--name',
nargs='?',
required=True,
help='Name of the center')
parsed_args = parser.parse_args(args)
live_name = parsed_args.name
payload = {'name': live_name}
res = rest.post('/oglive/delete', payload=payload)
if not res:
return 1
local_live_dir = OgLive._get_local_live_dir()
if not local_live_dir:
return 1
local_dir = os.path.join(local_live_dir, live_name)
if os.path.exists(local_dir):
try:
shutil.rmtree(local_dir)
except OSError as e:
print(f'Error deleting directory {local_dir}: {e}')
return 1
else:
print(f'Error: no directory found for {live_name}')
return 0
@staticmethod
def set_live(rest, args):
parser = argparse.ArgumentParser(prog='ogcli set live')
parser.add_argument('--default',
action='store_true',
required=True,
help='set the default live image')
parser.add_argument('--name',
nargs='?',
required=True,
help='Name of the live')
parsed_args = parser.parse_args(args)
live_name = parsed_args.name
payload = {'name': live_name}
res = rest.post('/oglive/default', payload=payload)
if not res:
return 1
return 0

View File

@ -14,8 +14,11 @@ class OgModes():
@staticmethod
def list_available_modes(rest):
r = rest.get('/mode')
print_json(r.text)
res = rest.get('/mode')
if not res:
return 1
print_json(res.text)
return 0
@staticmethod
def set_modes(rest, args):
@ -44,8 +47,8 @@ class OgModes():
help='Boot mode to be set')
parsed_args = parser.parse_args(args)
r = rest.get('/scopes')
scopes = r.json()
res = rest.get('/scopes')
scopes = res.json()
ips = set()
for center in parsed_args.center_id:
@ -59,7 +62,10 @@ class OgModes():
if not ips:
print("Missing --client-ip, or --room-id/--center-id. No clients provided.")
return None
return 1
payload = {'clients': list(ips), 'mode': parsed_args.mode[0]}
r = rest.post('/mode', payload=payload)
res = rest.post('/mode', payload=payload)
if not res:
return 1
return 0

View File

@ -36,8 +36,8 @@ class OgPoweroff():
help='Specific client IP')
parsed_args = parser.parse_args(args)
r = rest.get('/scopes')
scopes = r.json()
res = rest.get('/scopes')
scopes = res.json()
ips = set()
for center in parsed_args.center_id:
@ -51,7 +51,10 @@ class OgPoweroff():
if not ips:
print("Missing --client-ip, or --room-id/--center-id. No clients provided.")
return None
return 1
payload = {'clients': list(ips)}
r = rest.post('/poweroff', payload=payload)
res = rest.post('/poweroff', payload=payload)
if not res:
return 1
return 0

View File

@ -36,8 +36,8 @@ class OgReboot():
help='Specific client IP')
parsed_args = parser.parse_args(args)
r = rest.get('/scopes')
scopes = r.json()
res = rest.get('/scopes')
scopes = res.json()
ips = set()
for center in parsed_args.center_id:
@ -51,7 +51,10 @@ class OgReboot():
if not ips:
print("Missing --client-ip, or --room-id/--center-id. No clients provided.")
return None
return 1
payload = {'clients': list(ips)}
r = rest.post('/reboot', payload=payload)
res = rest.post('/reboot', payload=payload)
if not res:
return 1
return 0

View File

@ -13,8 +13,11 @@ class OgRepo():
@staticmethod
def list_repos(rest):
r = rest.get('/repositories')
print_json(r.text)
res = rest.get('/repositories')
if not res:
return 1
print_json(res.text)
return 0
@staticmethod
def _add_repo(rest, parsed_args):
@ -22,37 +25,43 @@ class OgRepo():
'addr': parsed_args.ip,
'name': parsed_args.name,
}
rest.post('/repository/add', payload=payload)
res = rest.post('/repository/add', payload=payload)
if not res:
return 1
return 0
@staticmethod
def _add_repo_ip(rest, parsed_args):
for ip in parsed_args.ip:
if not check_address(ip):
print(f'Invalid IP address: {ip}')
return
return 1
r = rest.get('/repositories')
res = rest.get('/repositories')
target_repo = None
for repo in r.json()['repositories']:
for repo in res.json()['repositories']:
if repo['id'] == parsed_args.id:
target_repo = repo
break
if not target_repo:
print('Invalid repository id specified')
return
return 1
for ip in parsed_args.ip:
if ip in target_repo['addr']:
print(f'The repository already contains the address {ip}')
return
return 1
payload = {
'id': parsed_args.id,
'addr': target_repo['addr'] + parsed_args.ip,
'name': target_repo['name'],
}
rest.post('/repository/update', payload=payload)
res = rest.post('/repository/update', payload=payload)
if not res:
return 1
return 0
@staticmethod
def add_repo(rest, args):
@ -101,30 +110,33 @@ class OgRepo():
'name': parsed_args.name,
}
rest.post('/repository/update', payload=payload)
res = rest.post('/repository/update', payload=payload)
if not res:
return 1
return 0
@staticmethod
def _delete_repo_ip(rest, parsed_args):
for ip in parsed_args.ip:
if not check_address(ip):
print(f'Invalid IP address: {ip}')
return
return 1
r = rest.get('/repositories')
res = rest.get('/repositories')
target_repo = None
for repo in r.json()['repositories']:
for repo in res.json()['repositories']:
if repo['id'] == parsed_args.id:
target_repo = repo
break
if not target_repo:
print('Invalid repository id specified')
return
return 1
for ip in parsed_args.ip:
if ip not in target_repo['addr']:
print(f'Invalid address {ip}: The repository has the following IPs: {target_repo["addr"]}')
return
return 1
target_repo['addr'].remove(ip)
@ -133,12 +145,18 @@ class OgRepo():
'addr': target_repo['addr'],
'name': target_repo['name'],
}
rest.post('/repository/update', payload=payload)
res = rest.post('/repository/update', payload=payload)
if not res:
return 1
return 0
@staticmethod
def _delete_repo(rest, parsed_args):
payload = {'id': parsed_args.id}
rest.post('/repository/delete', payload=payload)
res = rest.post('/repository/delete', payload=payload)
if not res:
return 1
return 0
@staticmethod
def delete_repo(rest, args):
@ -184,8 +202,8 @@ class OgRepo():
help='Any valid client ip address')
parsed_args = parser.parse_args(args)
r = rest.get('/scopes')
scopes = r.json()
res = rest.get('/scopes')
scopes = res.json()
ips = set()
for room in parsed_args.room_id:
@ -195,7 +213,10 @@ class OgRepo():
ips.add(l)
if not ips:
print('No valid clients specified.')
return
return 1
payload = {'id': parsed_args.id, 'clients': list(ips)}
rest.post('/client/repo', payload=payload)
res = rest.post('/client/repo', payload=payload)
if not res:
return 1
return 0

View File

@ -65,7 +65,7 @@ class OgRoom():
err = True
if err:
parser.print_help(file=sys.stderr)
sys.exit(1)
return 1
payload = {
'name': parsed_args.name,
@ -82,7 +82,10 @@ class OgRoom():
payload['folder_id'] = parsed_args.folder
if parsed_args.desc:
payload['location'] = parsed_args.desc
rest.post('/room/add', payload=payload)
res = rest.post('/room/add', payload=payload)
if not res:
return 1
return 0
@staticmethod
def update_room(rest, args):
@ -115,7 +118,7 @@ class OgRoom():
err = True
if err:
parser.print_help(file=sys.stderr)
sys.exit(1)
return 1
payload = {
'id': parsed_args.id,
@ -124,7 +127,10 @@ class OgRoom():
'gateway': parsed_args.gateway,
}
rest.post('/room/update', payload=payload)
res = rest.post('/room/update', payload=payload)
if not res:
return 1
return 0
@staticmethod
def delete_room(rest, args):
@ -136,5 +142,8 @@ class OgRoom():
help='room id in database')
parsed_args = parser.parse_args(args)
payload = {'id': parsed_args.id}
rest.post('/room/delete', payload=payload)
res = rest.post('/room/delete', payload=payload)
if not res:
return 1
return 0

View File

@ -41,7 +41,7 @@ class OgScope():
@staticmethod
def list_scopes(rest, args):
parser = argparse.ArgumentParser(prog='ogcli list scope')
parser = argparse.ArgumentParser(prog='ogcli list scopes')
group = parser.add_mutually_exclusive_group(required=False)
group.add_argument('--client-ip',
action='append',
@ -57,8 +57,8 @@ class OgScope():
for ip in parsed_args.client_ip:
ips.add(ip)
r = rest.get('/scopes')
json_data = json.loads(r.text)
res = rest.get('/scopes')
json_data = json.loads(res.text)
if parsed_args.name:
path = _get_client_path(json_data, None, parsed_args.name)
@ -71,8 +71,7 @@ class OgScope():
path = _get_client_path(json_data, client_ip, None)
for i, item in enumerate(path):
print(' ' * i + item)
return None
else:
print_json(r.text)
return None
print_json(res.text)
return 0

View File

@ -13,8 +13,11 @@ class OgServer():
@staticmethod
def list_servers(rest):
r = rest.get('/server')
print_json(r.text)
res = rest.get('/server')
if not res:
return 1
print_json(res.text)
return 0
@staticmethod
def add_server(rest, args):
@ -27,10 +30,13 @@ class OgServer():
if not check_address(parsed_args.ip):
print(f'Invalid IP address: {parsed_args.ip}')
return
return 1
payload = {'address': parsed_args.ip}
rest.post('/server', payload=payload)
res = rest.post('/server', payload=payload)
if not res:
return 1
return 0
@staticmethod
def delete_server(rest, args):
@ -42,4 +48,7 @@ class OgServer():
help='server id in the database')
parsed_args = parser.parse_args(args)
payload = {'id': parsed_args.id}
rest.delete('/server', payload=payload)
res = rest.delete('/server', payload=payload)
if not res:
return 1
return 0

View File

@ -44,8 +44,8 @@ class OgSession():
help='Specific client IP')
parsed_args = parser.parse_args(args)
r = rest.get('/scopes')
scopes = r.json()
res = rest.get('/scopes')
scopes = res.json()
ips = set()
for center in parsed_args.center_id:
@ -59,11 +59,14 @@ class OgSession():
if not ips:
print("Missing --client-ip, or --room-id/--center-id. No clients provided.")
return None
return 1
payload = {
'clients': list(ips),
'disk': parsed_args.disk,
'partition': parsed_args.part,
}
r = rest.post('/session', payload=payload)
res = rest.post('/session', payload=payload)
if not res:
return 1
return 0

View File

@ -53,8 +53,8 @@ class OgWol():
help='Specific client IP')
parsed_args = parser.parse_args(args)
r = rest.get('/scopes')
scopes = r.json()
res = rest.get('/scopes')
scopes = res.json()
ips = set()
for center in parsed_args.center_id:
@ -68,7 +68,10 @@ class OgWol():
if not ips:
print("Missing --client-ip, or --room-id/--center-id. No clients provided.")
return None
return 1
payload = {'type': parsed_args.type, 'clients': list(ips)}
r = rest.post('/wol', payload=payload)
res = rest.post('/wol', payload=payload)
if not res:
return 1
return 0

View File

@ -8,7 +8,9 @@
import unicodedata
import json
import ipaddress
import hashlib
import re
import os
def scope_lookup(scope_id, scope_type, d):
if scope_id == d.get('id') and scope_type == d.get('type'):
@ -47,8 +49,11 @@ def reorder_json_tree(payload):
payload[k] = val
reorder_json_tree(val)
def print_json(text):
payload = json.loads(text)
def print_json(data):
if isinstance(data, (dict, list)):
payload = data
else:
payload = json.loads(data)
reorder_json_tree(payload)
print(json.dumps(payload, indent=2, ensure_ascii=False))
@ -69,3 +74,21 @@ def check_mac_address(addr):
def remove_accents(text):
normalized_text = unicodedata.normalize('NFD', text)
return ''.join(c for c in normalized_text if unicodedata.category(c) != 'Mn')
def compute_md5(path, bs=2**20):
if not os.path.exists(path):
print(f"Failed to calculate checksum, image file {path} does not exist")
return None
m = hashlib.md5()
try:
with open(path, 'rb') as f:
while True:
buf = f.read(bs)
if not buf:
break
m.update(buf)
except Exception as e:
print(f'Failed to calculate checksum for {path}: {e}')
return None
return m.hexdigest()

21
ogcli
View File

@ -15,8 +15,6 @@ import argparse
import json
import sys
OG_CLI_CFG_PATH = "/opt/opengnsys/etc/ogcli.json"
def sigint_handler(signum, frame):
print("User has pressed ctrl-C, interrupting...")
sys.exit(1)
@ -25,22 +23,8 @@ class CLI():
def __init__(self):
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
signal.signal(signal.SIGINT, sigint_handler)
try:
with open(OG_CLI_CFG_PATH, 'r') as json_file:
self.cfg = json.load(json_file)
except json.JSONDecodeError:
sys.exit(f'ERROR: Failed parse malformed JSON file '
f'{OG_CLI_CFG_PATH}')
except:
sys.exit(f'ERROR: cannot open {OG_CLI_CFG_PATH}')
required_cfg_params = {'api_token', 'ip', 'port'}
difference_cfg_params = required_cfg_params - self.cfg.keys()
if len(difference_cfg_params) > 0:
sys.exit(f'Missing {difference_cfg_params} key in '
f'json config file')
self.ogcli = OgCLI(self.cfg)
self.ogcli = OgCLI()
parser = argparse.ArgumentParser(prog='ogcli')
parser.add_argument('command', help='Subcommand to run', nargs='?',
@ -59,7 +43,8 @@ class CLI():
sys.exit(1)
# Call the command with the same name.
getattr(self.ogcli, args.command)(sys.argv[2:])
res = getattr(self.ogcli, args.command)(sys.argv[2:])
sys.exit(res)
if __name__ == "__main__":