????

Your IP : 3.145.2.6


Current Path : /lib/Acronis/BackupAndRecovery/
Upload File :
Current File : //lib/Acronis/BackupAndRecovery/create_hosting_panel_client.py

#!/usr/sbin/acropsh
from argparse import ArgumentParser
import subprocess
import base64
import json
import uuid
import sys
import os

import xml.etree.ElementTree as ET

try:
    from urllib.parse import urlencode
    from urllib.request import urlopen, Request
    from urllib.error import HTTPError
except ImportError:
    from urllib import urlencode
    from urllib2 import urlopen, Request, HTTPError


class ClientCreator:
    def __init__(self, cred1, cred2, bearer_token, ve_ids):
        self.cred1 = cred1
        self.cred2 = cred2
        self.bearer_token = bearer_token
        self.group_id = ''
        self.tenant_id = ''
        self.ve_ids = ve_ids
        self.headers = {}
        self.dc_url = ''
        self.mc_url = ''

    def run(self):
        print('\nConnecting to the Cyber Protect service...')
        self.identify_url_and_group()
        if not self.bearer_token:
            self.dc_login()
            print('Successfully authenticated with the Cyber Protection service.')
        else:
            self.headers = {'Authorization': 'Bearer {}'.format(self.bearer_token),
                'Content-Type': 'application/json'}
        self.group_to_tenant()
        self.id_vz_ve_to_process()
        self.process_api_clients()

    def identify_url_and_group(self):
        parts = subprocess.check_output(['aakore', 'info']).decode('utf-8').strip().split('cloud server: ')[1].split(' ')
        self.dc_url = parts[0]
        self.mc_url = '{}/api/2/'.format(self.dc_url)
        self.group_id = parts[-1]

    def dc_login(self):
        is_client_auth = False
        try:
            uuid.UUID(str(self.cred1))
            is_client_auth = True
        except:
            pass

        try:
            if not is_client_auth:
                payload = {'grant_type': 'password', 'username': self.cred1, 'password': self.cred2}
                login_response = urlopen(Request('{}idp/token'.format(self.mc_url),
                                                data=urlencode(payload).encode('utf-8')))
            else:
                payload = {'grant_type': 'client_credentials'}
                req = Request('{}idp/token'.format(self.mc_url), data=urlencode(payload).encode('utf-8'))
                req.add_header("Authorization", "Basic {}".format(base64.b64encode('{}:{}'.format(self.cred1, self.cred2).encode('utf-8')).decode('utf-8')))
                login_response = urlopen(req)
        except HTTPError as e:
            if 'totp_required' in e.read().decode('utf-8'):
                totp_code = input(
                    '\nThis account is protected with two-factor authentication. Please enter the TOTP code: ')
                login_response = urlopen(Request('{}idp/token'.format(self.mc_url), data=urlencode(
                    {'grant_type': 'password', 'username': self.cred1,
                        'password': self.cred2, 'totp_code': totp_code}
                ).encode('utf-8')))
                if login_response.getcode() != 200:
                    print(
                        "Failed to obtain an access token. Please check your credentials and try again.")
                    sys.exit()
            else:
                raise e
        except Exception as e:
            print("Failed to create an access token: {}".format(e))
            sys.exit()
        if login_response.getcode() != 200:
            if json.loads(login_response.read().decode('utf-8')).get("error") == "totp_required":
                totp_code = input(
                    '\nThis account is protected with two-factor authentication. Please enter the TOTP code: ')
                login_response = urlopen(Request('{}idp/token'.format(self.mc_url), data=urlencode(
                    {'grant_type': 'password', 'username': self.username,
                        'password': self.password, 'totp_code': totp_code}
                ).encode('utf-8')))
                if login_response.getcode() != 200:
                    print(
                        "Failed to obtain an access token. Please check your credentials and try again.")
                    sys.exit()
            else:
                print("Failed to obtain an access token. Please check your credentials and try again.")
                sys.exit()
        self.headers = {'Authorization': 'Bearer {}'.format(json.loads(login_response.read().decode('utf-8')).get('access_token', '')),
                        'Content-Type': 'application/json'}

    def group_to_tenant(self):
        group_req = Request('{}/api/1/groups/{}'.format(self.dc_url, self.group_id), None, self.headers)
        self.tenant_id = json.loads(urlopen(group_req).read().decode('utf-8')).get('uuid')

    def id_vz_ve_to_process(self):
        if self.ve_ids:
            self.ve_ids = self.ve_ids.split(',')
            return
        else:
            self.ve_ids = []
            if os.path.isdir("/vz/root/"):
                try:
                    for line in subprocess.check_output(['prlctl', 'list', '-o', 'uuid', '-H']).decode('utf-8').splitlines():
                        self.ve_ids.append(line.strip().strip('{}'))
                except FileNotFoundError:
                    return

    def process_api_clients(self):
        if self.ve_ids:
            for ve in self.ve_ids:
                try:
                    self.process_api_client(ve)
                except Exception as err:
                    print("Failed to create client for VE {}: {}".format(ve, err))

        else:
            try:
                self.process_api_client()
            except Exception as err:
                print("Failed to create an agent client or save it to file. {}".format(err))

    def process_api_client(self, ve_id=None):
        plugin_srv_path = self.get_secret_file_path(ve_id)
        if not plugin_srv_path:
            print('Unable to detect a hosting control panel. Skipping...')
            return
        client_path = os.path.join(plugin_srv_path, 'client_secret.json')
        res_id = ve_id if ve_id else self.get_res_id_from_agent_config()
        client_id_from_existing_sw_pol = self.client_id_from_existing_sw_policy(res_id)
        client_data = self.get_client_creds(client_path, ve_id)
        if client_data:
            client_id = client_data.get('client_id')
            if client_id:
                if client_id_from_existing_sw_pol and client_id == client_id_from_existing_sw_pol:
                    if self.client_exists(client_id_from_existing_sw_pol):
                        if self.client_is_authorizable(client_data):
                            return
                        else:
                            self.recreate_and_save(client_id, client_path, ve_id)
                            return
                else:
                    if self.client_exists(client_id) and self.is_hosting_panel_agent_type(client_id):
                        self.delete_client_from_cloud(client_id)
        if client_id_from_existing_sw_pol and self.client_exists(client_id_from_existing_sw_pol):
            self.recreate_and_save(client_id_from_existing_sw_pol, client_path, ve_id)
            return
        self.create_client_and_save(client_path, ve_id, client_id_from_existing_sw_pol)

    def get_client_creds(self, client_path, ve_id=None):
        if not ve_id:
            try:
                with open(client_path) as client_file:
                    return json.load(client_file)
            except:
                return {}
        else:
            try:
                return json.loads(subprocess.check_output(['prlctl', 'exec', ve_id, 'cat', client_path, '2>/dev/null']).decode('utf-8').strip())
            except subprocess.CalledProcessError:
                return {}

    def client_exists(self, client_id):
        client_request = Request('{}/api/2/clients/{}'.format(self.dc_url, client_id), None, self.headers)
        try:
            return urlopen(client_request).getcode() == 200
        except HTTPError:
            return False

    def client_is_authorizable(self, client_data):
        payload = {'grant_type': 'client_credentials'}
        req = Request('{}idp/token'.format(self.mc_url), data=urlencode(payload).encode('utf-8'))
        req.add_header("Authorization", "Basic {}".format(base64.b64encode('{}:{}'.format(client_data.get('client_id'), client_data.get('client_secret')).encode('utf-8')).decode('utf-8')))
        try:
            return urlopen(req).getcode() == 200
        except HTTPError:
            return False

    def create_client_and_save(self, path, ve_id=None, client_id=None):
        client_data = self.create_client(ve_id, client_id)
        try:
            self.save_client(client_data, path, ve_id)
        except Exception as e:
            self.delete_client_from_cloud(client_data['client_id'])
            raise e

    def recreate_and_save(self, client_id, path, ve_id=None):
        self.delete_client_from_cloud(client_id)
        self.create_client_and_save(client_id, path, ve_id)

    def check_file_exists(self, path, ve_id=None):
        if not ve_id:
            return os.path.exists(path)
        try:
            subprocess.check_output(['prlctl', 'exec', ve_id, 'ls', path])
            return True
        except subprocess.CalledProcessError:
            return False

    def get_res_id_from_agent_config(self):
        tree = ET.parse('/etc/Acronis/BackupAndRecovery.config')
        root = tree.getroot()
        return root.findall(".//value[@name='InstanceID']")[0].text.replace('"','')

    def compile_client_name(self, ve_id=None):
        if not ve_id:
            return "Instance ID: {}".format(self.get_res_id_from_agent_config())
        else:
            return "Virtuozzo Instance ID: {}".format(ve_id.upper())

    def is_hosting_panel_agent_type(self, client_id):
        client_request = Request('{}/api/2/clients/{}'.format(self.dc_url, client_id), None, self.headers)
        client_response = urlopen(client_request)
        parsed_response = json.loads(client_response.read().decode('utf-8'))
        return parsed_response.get('data', {}).get('agent_type', '') == 'hosting_panel'

    def client_id_from_existing_sw_policy(self, ve_id):
        if not ve_id:
            return None
        app_request = Request(
            '{}/api/policy_management/v4/applications?context_id={}&policy_type=policy.backup.machine'.format(self.dc_url, ve_id),
            None,
            self.headers
        )
        app_response = urlopen(app_request)
        parsed_app_response = json.loads(app_response.read().decode('utf-8'))
        for comp_app in parsed_app_response.get('items', []):
            for app in comp_app:
                if 'policy.backup.' in app.get('policy', {}).get('type', ''):
                    pol_request = Request(
                        '{}/api/policy_management/v4/policies/{}'.format(self.dc_url, app.get('policy', {}).get('id')),
                        None,
                        self.headers
                    )
                    pol_response = urlopen(pol_request)
                    parsed_pol_response = json.loads(pol_response.read().decode('utf-8'))
                    policy = parsed_pol_response.get('policy', [])[0]
                    if 'client_id' in policy:
                        return policy['client_id']
        return None

    def create_client(self, ve_id=None, client_id=None):
        creation_request_body = {
            "type": "agent",
            "tenant_id": self.tenant_id,
            "data": {
                "client_name": self.compile_client_name(ve_id),
                "agent_type": "hosting_panel"
            },
            "token_endpoint_auth_method": "client_secret_basic"
        }
        if client_id:
            creation_request_body['client_id'] = client_id
        creation_request = Request(
            '{}/bc/api/account_server/v2/clients'.format(self.dc_url),
            json.dumps(creation_request_body).encode('utf-8'),
            self.headers
        )
        creation_response = urlopen(creation_request)
        if creation_response.getcode() == 201:
            parsed_response = json.loads(creation_response.read().decode('utf-8'))
            return {key:parsed_response[key] for key in ['client_id', 'client_secret']}
        else:
            raise Exception("Internal error occurred while creating an agent client: {}".format(
                creation_response.getcode()))

    def delete_client_from_cloud(self, client_id):
        client_deletion_request = Request('{}/api/2/clients/{}'.format(self.dc_url, client_id), None, self.headers)
        client_deletion_request.get_method = lambda: 'DELETE'
        client_deletion_response = urlopen(client_deletion_request)
        if client_deletion_response.getcode() != 204:
            print("Failed to delete API client {} from the Cyber Protection service.".format(client_id))

    def get_secret_file_path(self, ve):
        paths = [
            '/usr/local/cpanel/base/3rdparty/acronisbackup/srv',
            '/usr/local/directadmin/plugins/acronisbackup/srv',
            '/usr/local/psa/var/modules/acronis-backup/srv'
        ]
        if not ve:
            for path in paths:
                if os.path.isdir(path):
                    return path
        else:
            for path in paths:
                try:
                    subprocess.check_output(['prlctl', 'exec', ve, 'ls', path, '>/dev/null', '2>&1'])
                    return path
                except subprocess.CalledProcessError:
                    pass
        return None

    def save_client(self, client_data, path, ve_id):
        client_data['url'] = self.dc_url
        client_secret_file_content = json.dumps(client_data, sort_keys=True, separators=(',', ':'))
        if not ve_id:
            try:
                with open(path, 'w') as f:
                    f.write(client_secret_file_content)
                os.chmod(path, 0o660)
            except Exception as e:
                raise Exception(
                    "Failed to save credentials to file {}".format(path))
        else:
            try:
                subprocess.check_output(['prlctl', 'exec', ve_id, 'echo', "'{}'".format(client_secret_file_content), '>', path])
            except Exception as e:
                raise Exception(
                    "Failed to save credentials for VE {}".format(ve_id))

        if self.check_file_exists(path, ve_id):
            print("Agent client was successfully created{}".format(' for ' + ve_id if ve_id else '.'))


def main(args):
    cred1 = ''
    cred2 = os.environ.get('ACRONIS_HOSTING_SECRET', '')
    bearer_token = os.environ.get('ACRONIS_HOSTING_BEARER_TOKEN', '')
    if args.username:
        cred1 = args.username
        if not cred2 and args.password_file:
            with open(args.password_file) as f:
                cred2 = f.readline().strip()

    if args.client_id:
        cred1 = args.client_id
        if not cred2 and args.secret_file:
            with open(args.secret_file) as f:
                cred2 = f.readline().strip()

    if not cred1 and not cred2 and not bearer_token:
        print('Authentication with the Cyber Protection service is required. \
              Please use either --username and --password-file or --client-id and --secret-file.')
        sys.exit()

    cl_creator = ClientCreator(cred1, cred2, bearer_token, args.ve_id)
    cl_creator.run()
    print('Done! Good bye.')


def parse_arguments():
    parser = ArgumentParser(
        description='This script generates unique API Clients necessary to ensure the connectivity of hosting control panel integrations with Acronis Cyber Protect Cloud. Make sure to run this script on the host where the protection agent is installed. This applies to both agent-based and agentless deployments.')
    parser.add_argument('-u', '--username', help='Account name (login) used during the protection agent installation and registration process.')
    parser.add_argument('-p', '--password-file',
                        help='Path to the file with the password.')
    parser.add_argument('-i', '--client-id', help='Client ID used during the protection agent installation and registration process.')
    parser.add_argument('-s', '--secret-file', help='Path to the file with the Client Secret.')
    parser.add_argument('-c', '--ve-id',
                        help='Comma-separated list of virtual environment (container, virtual machine) IDs. If not defined, all virtual environments will be processed.')

    return parser.parse_args()


if __name__ == '__main__':
    main(parse_arguments())