import logging import socket import oca from django.conf import settings from oca.exceptions import OpenNebulaException from oca.pool import WrongNameError, WrongIdError from hosting.models import HostingOrder from utils.models import CustomUser from utils.tasks import save_ssh_key, save_ssh_key_error_handler from .exceptions import KeyExistsError, UserExistsError, UserCredentialError logger = logging.getLogger(__name__) class OpenNebulaManager(): """This class represents an opennebula manager.""" def __init__(self, email=None, password=None): self.email = email self.password = password # Get oneadmin client self.oneadmin_client = self._get_opennebula_client( settings.OPENNEBULA_USERNAME, settings.OPENNEBULA_PASSWORD ) # Get or create oppenebula user using given credentials try: self.opennebula_user = self._get_or_create_user( email, password ) # If opennebula user was created/obtained, get his client self.client = self._get_opennebula_client( email, password ) except: pass def _get_client(self, user): """Get a opennebula client object for a CustomUser object Args: user (CustomUser): dynamicweb CustomUser object Returns: oca.Client: Opennebula client object Raise: ConnectionError: If the connection to the opennebula server can't be established """ return oca.Client("{0}:{1}".format( user.email, user.password), "{protocol}://{domain}:{port}{endpoint}".format( protocol=settings.OPENNEBULA_PROTOCOL, domain=settings.OPENNEBULA_DOMAIN, port=settings.OPENNEBULA_PORT, endpoint=settings.OPENNEBULA_ENDPOINT )) def _get_opennebula_client(self, username, password): return oca.Client("{0}:{1}".format( username, password), "{protocol}://{domain}:{port}{endpoint}".format( protocol=settings.OPENNEBULA_PROTOCOL, domain=settings.OPENNEBULA_DOMAIN, port=settings.OPENNEBULA_PORT, endpoint=settings.OPENNEBULA_ENDPOINT )) def _get_user(self, user): """Get the corresponding opennebula user for a CustomUser object Args: user (CustomUser): dynamicweb CustomUser object Returns: oca.User: Opennebula user object Raise: WrongNameError: If no openebula user with this credentials exists ConnectionError: If the connection to the opennebula server can't be established """ user_pool = self._get_user_pool() return user_pool.get_by_name(user.email) def create_user(self, user: CustomUser): """Create a new opennebula user or a corresponding CustomUser object Args: user (CustomUser): dynamicweb CustomUser object Returns: int: Return the opennebula user id Raises: ConnectionError: If the connection to the opennebula server can't be established UserExistsError: If a user with this credeintals already exits on the server UserCredentialError: If a user with this email exists but the password is worng """ try: self._get_user(user) try: self._get_client(self, user) logger.debug('User already exists') raise UserExistsError() except OpenNebulaException as err: logger.error('OpenNebulaException error: {0}'.format(err)) logger.debug('User exists but password is wrong') raise UserCredentialError() except WrongNameError: user_id = self.oneadmin_client.call(oca.User.METHODS['allocate'], user.email, user.password, 'core') logger.debug( 'Created a user for CustomObject: {user} with user id = {u_id}', user=user, u_id=user_id ) return user_id except ConnectionRefusedError: logger.error( 'Could not connect to host: {host} via protocol {protocol}'.format( host=settings.OPENNEBULA_DOMAIN, protocol=settings.OPENNEBULA_PROTOCOL) ) raise ConnectionRefusedError def _get_or_create_user(self, email, password): try: user_pool = self._get_user_pool() opennebula_user = user_pool.get_by_name(email) return opennebula_user except WrongNameError as wrong_name_err: opennebula_user = self.oneadmin_client.call( oca.User.METHODS['allocate'], email, password, 'core') logger.debug( "User {} does not exist. Created the user. User id = {}".format( email, opennebula_user ) ) return opennebula_user except ConnectionRefusedError: logger.info( 'Could not connect to host: {host} via protocol {protocol}'.format( host=settings.OPENNEBULA_DOMAIN, protocol=settings.OPENNEBULA_PROTOCOL) ) raise ConnectionRefusedError def _get_user_pool(self): try: user_pool = oca.UserPool(self.oneadmin_client) user_pool.info() except ConnectionRefusedError: logger.info( 'Could not connect to host: {host} via protocol {protocol}'.format( host=settings.OPENNEBULA_DOMAIN, protocol=settings.OPENNEBULA_PROTOCOL) ) raise return user_pool def _get_vm_pool(self): try: vm_pool = oca.VirtualMachinePool(self.client) vm_pool.info() return vm_pool except AttributeError: logger.info('Could not connect via client, using oneadmin instead') try: vm_pool = oca.VirtualMachinePool(self.oneadmin_client) vm_pool.info(filter=-2) return vm_pool except: raise ConnectionRefusedError except ConnectionRefusedError: logger.info( 'Could not connect to host: {host} via protocol {protocol}'.format( host=settings.OPENNEBULA_DOMAIN, protocol=settings.OPENNEBULA_PROTOCOL) ) raise ConnectionRefusedError # For now we'll just handle all other errors as connection errors except: raise ConnectionRefusedError def get_vms(self): try: return self._get_vm_pool() except ConnectionRefusedError: raise ConnectionRefusedError def get_vm(self, vm_id): vm_id = int(vm_id) try: vm_pool = self._get_vm_pool() return vm_pool.get_by_id(vm_id) except WrongIdError: raise WrongIdError except: raise ConnectionRefusedError def is_public_ipv4(self, ipv4): """ Checks whether the passed ipv4 is a public IP. :param ipv4: :return: Returns true if it is a public IP else returns false """ if ipv4.startswith("10."): return False else: return True def get_primary_ip(self, vm_id): """ Returns primary ipv4 if it exists and is a public ip. Otherwise returns the IPv6 of the machine :param vm_id: :return: """ ipv4 = self.get_primary_ipv4(vm_id) if self.is_public_ipv4(ipv4): return ipv4 else: return self.get_primary_ipv6(vm_id) def get_primary_ipv6(self, vm_id): """ Returns the primary IPv6 of the given vm. For now, we return the first available IPv6 (to be changed later) :return: An IP address string, if it exists else returns None """ all_ipv6s = self.get_vm_ipv6_addresses(vm_id) if len(all_ipv6s) > 0: return all_ipv6s[0] else: return None def get_vm_ipv6_addresses(self, vm_id): """ Returns a list of IPv6 addresses of the given vm :param vm_id: The ID of the vm :return: """ ipv6s = [] vm = self.get_vm(vm_id) for nic in vm.template.nics: if hasattr(nic, 'ip6_global'): ipv6s.append(nic.ip6_global) return ipv6s def get_primary_ipv4(self, vm_id): """ Returns the primary IPv4 of the given vm. To be changed later. :return: An IP address string, if it exists else returns None """ all_ipv4s = self.get_vm_ipv4_addresses(vm_id) if len(all_ipv4s) > 0: return all_ipv4s[0] else: return None def get_vm_ipv4_addresses(self, vm_id): """ Returns a list of IPv4 addresses of the given vm :param vm_id: The ID of the vm :return: """ ipv4s = [] vm = self.get_vm(vm_id) for nic in vm.template.nics: if hasattr(nic, 'ip'): ipv4s.append(nic.ip) return ipv4s def create_vm(self, template_id, specs, ssh_key=None, vm_name=None): template = self.get_template(template_id) vm_specs_formatter = """ """ try: vm_id = self.client.call( oca.VmTemplate.METHODS['instantiate'], template.id, '', True, vm_specs, False ) except OpenNebulaException as err: logger.error("OpenNebulaException: {0}".format(err)) return None self.oneadmin_client.call( oca.VirtualMachine.METHODS['action'], 'release', vm_id ) if vm_name is not None: self.oneadmin_client.call( 'vm.rename', vm_id, vm_name ) return vm_id def delete_vm(self, vm_id): TERMINATE_ACTION = 'terminate' vm_terminated = False try: self.oneadmin_client.call( oca.VirtualMachine.METHODS['action'], TERMINATE_ACTION, int(vm_id), ) vm_terminated = True except socket.timeout as socket_err: logger.info("Socket timeout error: {0}".format(socket_err)) except OpenNebulaException as opennebula_err: logger.info( "OpenNebulaException error: {0}".format(opennebula_err)) except OSError as os_err: logger.info("OSError : {0}".format(os_err)) except ValueError as value_err: logger.info("ValueError : {0}".format(value_err)) return vm_terminated def _get_template_pool(self): try: template_pool = oca.VmTemplatePool(self.oneadmin_client) template_pool.info() return template_pool except ConnectionRefusedError: logger.info( """Could not connect to host: {host} via protocol {protocol}""".format( host=settings.OPENNEBULA_DOMAIN, protocol=settings.OPENNEBULA_PROTOCOL) ) raise ConnectionRefusedError except: raise ConnectionRefusedError def get_templates(self, prefix='public-'): try: public_templates = [ template for template in self._get_template_pool() if template.name.startswith(prefix) ] return public_templates except ConnectionRefusedError: raise ConnectionRefusedError except: raise ConnectionRefusedError def try_get_templates(self): try: return self.get_templates() except: return [] def get_template(self, template_id): template_id = int(template_id) try: template_pool = self._get_template_pool() return template_pool.get_by_id(template_id) except: raise ConnectionRefusedError def create_template(self, name, cores, memory, disk_size, core_price, memory_price, disk_size_price, ssh=''): """Create and add a new template to opennebula. :param name: A string representation describing the template. Used as label in view. :param cores: Amount of virtual cpu cores for the VM. :param memory: Amount of RAM for the VM (GB) :param disk_size: Amount of disk space for VM (GB) :param core_price: Price of virtual cpu for the VM per core. :param memory_price: Price of RAM for the VM per GB :param disk_size_price: Price of disk space for VM per GB :param ssh: User public ssh key """ template_string_formatter = """ """ template_id = oca.VmTemplate.allocate( self.oneadmin_client, template_string_formatter.format( name=name, vcpu=cores, cpu=0.1 * cores, size=1024 * disk_size, memory=1024 * memory, # * 10 because we set cpu to *0.1 cpu_cost=10 * core_price, memory_cost=memory_price, disk_cost=disk_size_price, ssh=ssh ) ) return template_id def delete_template(self, template_id): self.oneadmin_client.call(oca.VmTemplate.METHODS[ 'delete'], template_id, False) def change_user_password(self, passwd_hash): self.oneadmin_client.call( oca.User.METHODS['passwd'], self.opennebula_user.id, passwd_hash ) def add_public_key(self, user, public_key='', merge=False): """ Args: user (CustomUser): Dynamicweb user public_key (string): Public key to add to the user merge (bool): Optional if True the new public key replaces the old Raises: KeyExistsError: If replace is False and the user already has a public key WrongNameError: If no openebula user with this credentials exists ConnectionError: If the connection to the opennebula server can't be established Returns: True if public_key was added """ # TODO: Check if we can remove this first try because we basically just # raise the possible Errors try: open_user = self._get_user(user) try: old_key = open_user.template.ssh_public_key if not merge: raise KeyExistsError() public_key += '\n{key}'.format(key=old_key) except AttributeError: pass self.oneadmin_client.call('user.update', open_user.id, '{key}' .format(key=public_key)) return True except WrongNameError: raise except ConnectionError: raise def remove_public_key(self, user, public_key=''): """ Args: user (CustomUser): Dynamicweb user public_key (string): Public key to be removed to the user Raises: KeyDoesNotExistsError: If replace is False and the user already has a public key WrongNameError: If no openebula user with this credentials exists ConnectionError: If the connection to the opennebula server can't be established Returns: True if public_key was removed """ try: open_user = self._get_user(user) try: old_key = open_user.template.ssh_public_key if public_key not in old_key: return False # raise KeyDoesNotExistsError() if '\n{}'.format(public_key) in old_key: public_key = old_key.replace('\n{}'.format(public_key), '') else: public_key = old_key.replace(public_key, '') except AttributeError: return False # raise KeyDoesNotExistsError() self.oneadmin_client.call('user.update', open_user.id, '{key}' .format(key=public_key)) return True except WrongNameError: raise except ConnectionError: raise def manage_public_key(self, keys, hosts=None, countdown=0): """ A function that manages the supplied keys in the authorized_keys file of the given list of hosts. If hosts parameter is not supplied, all hosts of this customer will be configured with the supplied keys :param keys: A list of ssh keys that are to be added/removed A key should be a dict of the form { 'value': 'sha-.....', # public key as string 'state': True # whether key is to be added or } # removed :param hosts: A list of hosts IP addresses :param countdown: Parameter to be passed to celery apply_async Allows to delay a task by `countdown` number of seconds :return: """ if hosts is None: hosts = self.get_all_hosts() if len(hosts) > 0 and len(keys) > 0: save_ssh_key.apply_async((hosts, keys), countdown=countdown, link_error=save_ssh_key_error_handler.s()) else: logger.debug( "Keys and/or hosts are empty, so not managing any keys") def get_all_hosts(self): """ A utility function to obtain all hosts of this owner :return: A list of hosts IP addresses, empty if none exist """ owner = CustomUser.objects.filter( email=self.email).first() all_orders = HostingOrder.objects.filter(customer__user=owner) hosts = [] if len(all_orders) > 0: logger.debug("The user {} has 1 or more VMs. We need to configure " "the ssh keys.".format(self.email)) for order in all_orders: try: vm = self.get_vm(order.vm_id) for nic in vm.template.nics: if hasattr(nic, 'ip'): if str(nic.ip).startswith("10."): if hasattr(nic, 'ip6_global'): hosts.append(nic.ip6_global) else: hosts.append(nic.ip) except WrongIdError: logger.debug( "VM with ID {} does not exist".format(order.vm_id)) else: logger.debug("The user {} has no VMs. We don't need to configure " "the ssh keys.".format(self.email)) return hosts