class IPSingleMachineService(IPServiceBase): # Gui ip = gui.TextField(length=64, label=_('Machine IP'), order=1, tooltip=_('Machine IP'), required=True) # Description of service typeName = _('Static Single IP') typeType = 'IPSingleMachineService' typeDescription = _( 'This service provides access to POWERED-ON Machine by IP') iconFile = 'machine.png' # Characteristics of service maxDeployed = -1 # If the service provides more than 1 "provided service" (-1 = no limit, 0 = ???? (do not use it!!!), N = max number to deploy usesCache = False # Cache are running machine awaiting to be assigned usesCache_L2 = False # L2 Cache are running machines in suspended state needsManager = False # If the service needs a s.o. manager (managers are related to agents provided by services itselfs, i.e. virtual machines with agent) mustAssignManually = False # If true, the system can't do an automatic assignation of a deployed user service from this service deployedType = IPMachineDeployed servicesTypeProvided = (serviceTypes.VDI, ) def initialize(self, values: 'Module.ValuesType') -> None: if values is None: return if not net.isValidHost(self.ip.value): raise IPServiceBase.ValidationException( gettext('Invalid server used: "{}"'.format(self.ip.value))) def getUnassignedMachine(self) -> typing.Optional[str]: ip: typing.Optional[str] = None try: counter = self.storage.getPickle('counter') counter = counter + 1 if counter is not None else 1 self.storage.putPickle('counter', counter) ip = '{}~{}'.format(self.ip.value, counter) except Exception: ip = None logger.exception("Exception at getUnassignedMachine") return ip def unassignMachine(self, ip: str) -> None: pass
class HTML5RDPTransport(transports.Transport): """ Provides access via RDP to service. This transport can use an domain. If username processed by authenticator contains '@', it will split it and left-@-part will be username, and right password """ typeName = _('HTML5 RDP') typeType = 'HTML5RDPTransport' typeDescription = _('RDP protocol using HTML5 client') iconFile = 'html5.png' ownLink = True supportedOss = OsDetector.allOss protocol = transports.protocols.RDP group = transports.TUNNELED_GROUP guacamoleServer = gui.TextField(label=_('Tunnel Server'), order=1, tooltip=_('Host of the tunnel server (use http/https & port if needed) as accesible from users'), defvalue='https://', length=64, required=True, tab=gui.TUNNEL_TAB) useEmptyCreds = gui.CheckBoxField(label=_('Empty creds'), order=2, tooltip=_('If checked, the credentials used to connect will be emtpy'), tab=gui.CREDENTIALS_TAB) fixedName = gui.TextField(label=_('Username'), order=3, tooltip=_('If not empty, this username will be always used as credential'), tab=gui.CREDENTIALS_TAB) fixedPassword = gui.PasswordField(label=_('Password'), order=4, tooltip=_('If not empty, this password will be always used as credential'), tab=gui.CREDENTIALS_TAB) withoutDomain = gui.CheckBoxField(label=_('Without Domain'), order=5, tooltip=_('If checked, the domain part will always be emptied (to connecto to xrdp for example is needed)'), tab=gui.CREDENTIALS_TAB) fixedDomain = gui.TextField(label=_('Domain'), order=6, tooltip=_('If not empty, this domain will be always used as credential (used as DOMAIN\\user)'), tab=gui.CREDENTIALS_TAB) wallpaper = gui.CheckBoxField(label=_('Show wallpaper'), order=20, tooltip=_('If checked, the wallpaper and themes will be shown on machine (better user experience, more bandwidth)'), tab=gui.PARAMETERS_TAB) desktopComp = gui.CheckBoxField(label=_('Allow Desk.Comp.'), order=22, tooltip=_('If checked, desktop composition will be allowed'), tab=gui.PARAMETERS_TAB) smooth = gui.CheckBoxField(label=_('Font Smoothing'), order=23, tooltip=_('If checked, fonts smoothing will be allowed (windows clients only)'), tab=gui.PARAMETERS_TAB) enableAudio = gui.CheckBoxField(label=_('Enable Audio'), order=24, tooltip=_('If checked, the audio will be redirected to client (if client browser supports it)'), tab=gui.PARAMETERS_TAB) enablePrinting = gui.CheckBoxField(label=_('Enable Printing'), order=25, tooltip=_('If checked, the printing will be redirected to client (if client browser supports it)'), tab=gui.PARAMETERS_TAB) enableFileSharing = gui.CheckBoxField(label=_('Enable File Sharing'), order=8, tooltip=_('If checked, the user will be able to upload/download files (if client browser supports it)'), tab=gui.PARAMETERS_TAB) serverLayout = gui.ChoiceField( order=26, label=_('Layout'), tooltip=_('Keyboards Layout of server'), required=True, values=[ gui.choiceItem('-', 'default'), gui.choiceItem('en-us-qwerty', _('English (US) keyboard')), gui.choiceItem('en-gb-qwerty', _('English (GB) keyboard')), gui.choiceItem('es-es-qwerty', _('Spanish keyboard')), gui.choiceItem('latam-qwerty', _('Latin American keyboard')), gui.choiceItem('de-de-qwertz', _('German keyboard (qwertz)')), gui.choiceItem('fr-fr-azerty', _('French keyboard (azerty)')), gui.choiceItem('fr-ch-qwertz', _('Swiss French keyboard (qwertz)')), gui.choiceItem('de-ch-qwertz', _('Swiss German keyboard (qwertz)')), gui.choiceItem('it-it-qwerty', _('Italian keyboard')), gui.choiceItem('sv-se-qwerty', _('Swedish keyboard')), gui.choiceItem('ja-jp-qwerty', _('Japanese keyboard')), gui.choiceItem('pt-br-qwerty', _('Brazilian keyboard')), gui.choiceItem('failsafe', _('Failsafe')), ], defvalue='-', tab=gui.PARAMETERS_TAB ) security = gui.ChoiceField( order=27, label=_('Security'), tooltip=_('Connection security mode for Guacamole RDP connection'), required=True, values=[ gui.choiceItem('any', _('Any (Allow the server to choose the type of auth)')), gui.choiceItem('rdp', _('RDP (Standard RDP encryption. Should be supported by all servers)')), gui.choiceItem('nla', _('NLA (Network Layer authentication. Requires VALID username&password, or connection will fail)')), gui.choiceItem('tls', _('TLS (Transport Security Layer encryption)')), ], defvalue='rdp', tab=gui.PARAMETERS_TAB ) ticketValidity = gui.NumericField( length=3, label=_('Ticket Validity'), defvalue='60', order=90, tooltip=_('Allowed time, in seconds, for HTML5 client to reload data from UDS Broker. The default value of 60 is recommended.'), required=True, minValue=60, tab=gui.ADVANCED_TAB ) forceNewWindow = gui.CheckBoxField( label=_('Force new HTML Window'), order=91, tooltip=_('If checked, every connection will try to open its own window instead of reusing the "global" one.'), defvalue=gui.FALSE, tab=gui.ADVANCED_TAB ) def initialize(self, values: 'Module.ValuesType'): if not values: return # Strip spaces self.guacamoleServer.value = self.guacamoleServer.value.strip() if self.guacamoleServer.value[0:4] != 'http': raise transports.Transport.ValidationException(_('The server must be http or https')) if self.useEmptyCreds.isTrue() and self.security.value != 'rdp': raise transports.Transport.ValidationException(_('Empty credentials (on Credentials tab) is only allowed with Security level (on Parameters tab) set to "RDP"')) # Same check as normal RDP transport def isAvailableFor(self, userService: 'models.UserService', ip: str) -> bool: """ Checks if the transport is available for the requested destination ip Override this in yours transports """ logger.debug('Checking availability for %s', ip) ready = self.cache.get(ip) if not ready: # Check again for readyness if self.testServer(userService, ip, '3389') is True: self.cache.put(ip, 'Y', READY_CACHE_TIMEOUT) return True self.cache.put(ip, 'N', READY_CACHE_TIMEOUT) return ready == 'Y' def processedUser(self, userService: 'models.UserService', user: '******') -> str: v = self.processUserAndPassword(userService, user, '') return v['username'] def processUserAndPassword(self, userService: 'models.UserService', user: '******', password: str) -> typing.Dict[str, str]: username: str = user.getUsernameForAuth() if self.fixedName.value != '': username = self.fixedName.value proc = username.split('@') domain = proc[1] if len(proc) > 1 else '' username = proc[0] if self.fixedPassword.value != '': password = self.fixedPassword.value if self.fixedDomain.value != '': domain = self.fixedDomain.value if self.useEmptyCreds.isTrue(): username, password, domain = '', '', '' # If no domain to be transfered, set it to '' if self.withoutDomain.isTrue(): domain = '' if '.' in domain: # Dotter domain form username = username + '@' + domain domain = '' # Fix username/password acording to os manager username, password = userService.processUserPassword(username, password) return {'protocol': self.protocol, 'username': username, 'password': password, 'domain': domain} def getLink( # pylint: disable=too-many-locals self, userService: 'models.UserService', transport: 'models.Transport', ip: str, os: typing.Dict[str, str], user: '******', password: str, request: 'HttpRequest' ) -> str: credsInfo = self.processUserAndPassword(userService, user, password) username, password, domain = credsInfo['username'], credsInfo['password'], credsInfo['domain'] if domain != '': username = domain + '\\' + username scrambler = cryptoManager().randomString(32) passwordCrypted = cryptoManager().symCrypt(password, scrambler) # Build params dict params = { 'protocol': 'rdp', 'hostname': ip, 'username': username, 'password': passwordCrypted, 'ignore-cert': 'true', 'security': self.security.value, 'drive-path': '/share/{}'.format(user.uuid), 'create-drive-path': 'true' } if self.enableFileSharing.isTrue(): params['enable-drive'] = 'true' if self.serverLayout.value != '-': params['server-layout'] = self.serverLayout.value if self.enableAudio.isTrue() is False: params['disable-audio'] = 'true' if self.enablePrinting.isTrue() is True: params['enable-printing'] = 'true' params['printer-name'] = 'UDS-Printer' if self.wallpaper.isTrue() is True: params['enable-wallpaper'] = 'true' if self.desktopComp.isTrue() is True: params['enable-desktop-composition'] = 'true' if self.smooth.isTrue() is True: params['enable-font-smoothing'] = 'true' logger.debug('RDP Params: %s', params) ticket = models.TicketStore.create(params, validity=self.ticketValidity.num()) return HttpResponseRedirect( "{}/transport/{}?{}.{}&{}".format( self.guacamoleServer.value, 'o_n_w' if self.forceNewWindow.isTrue() else '', ticket, scrambler, 'javascript:window.close();' ) )
class LiveService(Service): """ OpenStack Live Service """ # : Name to show the administrator. This string will be translated BEFORE # : sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) typeName = _('OpenStack Live Volume') # : Type used internally to identify this provider typeType = 'openStackLiveService' # : Description shown at administration interface for this provider typeDescription = _('OpenStack live images based service') # : Icon file used as icon for this provider. This string will be translated # : BEFORE sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) iconFile = 'openstack.png' # Functional related data # : If the service provides more than 1 "deployed user" (-1 = no limit, # : 0 = ???? (do not use it!!!), N = max number to deploy maxDeployed = -1 # : If we need to generate "cache" for this service, so users can access the # : provided services faster. Is usesCache is True, you will need also # : set publicationType, do take care about that! usesCache = True # : Tooltip shown to user when this item is pointed at admin interface, none # : because we don't use it cacheTooltip = _( 'Number of desired machines to keep running waiting for an user') usesCache_L2 = False # L2 Cache are running machines in suspended state cacheTooltip_L2 = _( 'Number of desired machines to keep suspended waiting for use') # : If the service needs a s.o. manager (managers are related to agents # : provided by services itselfs, i.e. virtual machines with actors) needsManager = True # : If true, the system can't do an automatic assignation of a deployed user # : service from this service mustAssignManually = False canReset = True # : Types of publications (preparated data for deploys) # : In our case, we do no need a publication, so this is None publicationType = LivePublication # : Types of deploys (services in cache and/or assigned to users) deployedType = LiveDeployment allowedProtocols = protocols.GENERIC + (protocols.SPICE, ) servicesTypeProvided = (serviceTypes.VDI, ) # Now the form part region = gui.ChoiceField(label=_('Region'), order=1, tooltip=_('Service region'), required=True, rdonly=True) project = gui.ChoiceField(label=_('Project'), order=2, fills={ 'callbackName': 'osFillResources', 'function': helpers.getResources, 'parameters': ['ov', 'ev', 'project', 'region', 'legacy'] }, tooltip=_('Project for this service'), required=True, rdonly=True) availabilityZone = gui.ChoiceField( label=_('Availability Zones'), order=3, fills={ 'callbackName': 'osFillVolumees', 'function': helpers.getVolumes, 'parameters': ['ov', 'ev', 'project', 'region', 'availabilityZone', 'legacy'] }, tooltip=_('Service availability zones'), required=True, rdonly=True) volume = gui.ChoiceField( label=_('Volume'), order=4, tooltip=_('Base volume for service (restricted by availability zone)'), required=True, tab=_('Machine')) # volumeType = gui.ChoiceField(label=_('Volume Type'), order=5, tooltip=_('Volume type for service'), required=True) network = gui.ChoiceField(label=_('Network'), order=6, tooltip=_('Network to attach to this service'), required=True, tab=_('Machine')) flavor = gui.ChoiceField(label=_('Flavor'), order=7, tooltip=_('Flavor for service'), required=True, tab=_('Machine')) securityGroups = gui.MultiChoiceField(label=_('Security Groups'), order=8, tooltip=_('Service security groups'), required=True, tab=_('Machine')) baseName = gui.TextField( label=_('Machine Names'), rdonly=False, order=9, tooltip=_('Base name for clones from this machine'), required=True, tab=_('Machine')) lenName = gui.NumericField( length=1, label=_('Name Length'), defvalue=5, order=10, tooltip=_('Size of numeric part for the names of these machines'), required=True, tab=_('Machine')) ov = gui.HiddenField(value=None) ev = gui.HiddenField(value=None) legacy = gui.HiddenField( value=None ) # We need to keep the env so we can instantiate the Provider _api: typing.Optional['openstack.Client'] = None def initialize(self, values): """ We check here form values to see if they are valid. Note that we check them through FROM variables, that already has been initialized by __init__ method of base class, before invoking this. """ if values: tools.checkValidBasename(self.baseName.value, self.lenName.num()) # self.ov.value = self.parent().serialize() # self.ev.value = self.parent().env.key def parent(self) -> 'Provider': return typing.cast('Provider', super().parent()) def initGui(self): """ Loads required values inside """ api = self.parent().api() if not self.parent().legacy and self.parent().region.value: regions = [ gui.choiceItem(self.parent().region.value, self.parent().region.value) ] else: regions = [ gui.choiceItem(r['id'], r['id']) for r in api.listRegions() ] self.region.setValues(regions) if not self.parent().legacy and self.parent().tenant.value: tenants = [ gui.choiceItem(self.parent().tenant.value, self.parent().tenant.value) ] else: tenants = [ gui.choiceItem(t['id'], t['name']) for t in api.listProjects() ] self.project.setValues(tenants) # So we can instantiate parent to get API logger.debug(self.parent().serialize()) self.ov.setDefValue(self.parent().serialize()) self.ev.setDefValue(self.parent().env.key) self.legacy.setDefValue(self.parent().legacy and 'true' or 'false') @property def api(self) -> 'openstack.Client': if not self._api: self._api = self.parent().api(projectId=self.project.value, region=self.region.value) return self._api def sanitizeVmName(self, name: str) -> str: return self.parent().sanitizeVmName(name) def makeTemplate(self, templateName: str, description: typing.Optional[str] = None): # First, ensures that volume has not any running instances # if self.api.getVolume(self.volume.value)['status'] != 'available': # raise Exception('The Volume is in use right now. Ensure that there is no machine running before publishing') description = description or 'UDS Template snapshot' return self.api.createVolumeSnapshot(self.volume.value, templateName, description) def getTemplate(self, snapshotId: str): """ Checks current state of a template (an snapshot) """ return self.api.getSnapshot(snapshotId) def deployFromTemplate(self, name: str, snapshotId: str) -> str: """ Deploys a virtual machine on selected cluster from selected template Args: name: Name (sanitized) of the machine comments: Comments for machine snapshotId: Id of the snapshot to deploy from Returns: Id of the machine being created form template """ logger.debug('Deploying from template %s machine %s', snapshotId, name) # self.datastoreHasSpace() return self.api.createServerFromSnapshot( snapshotId=snapshotId, name=name, availabilityZone=self.availabilityZone.value, flavorId=self.flavor.value, networkId=self.network.value, securityGroupsIdsList=self.securityGroups.value)['id'] def removeTemplate(self, templateId: str) -> None: """ invokes removeTemplate from parent provider """ self.api.deleteSnapshot(templateId) def getMachineState(self, machineId: str) -> str: """ Invokes getServer from openstack client Args: machineId: If of the machine to get state Returns: one of this values: ACTIVE. The server is active. BUILDING. The server has not finished the original build process. DELETED. The server is permanently deleted. ERROR. The server is in error. HARD_REBOOT. The server is hard rebooting. This is equivalent to pulling the power plug on a physical server, plugging it back in, and rebooting it. MIGRATING. The server is being migrated to a new host. PASSWORD. The password is being reset on the server. PAUSED. In a paused state, the state of the server is stored in RAM. A paused server continues to run in frozen state. REBOOT. The server is in a soft reboot state. A reboot command was passed to the operating system. REBUILD. The server is currently being rebuilt from an image. RESCUED. The server is in rescue mode. A rescue image is running with the original server image attached. RESIZED. Server is performing the differential copy of data that changed during its initial copy. Server is down for this stage. REVERT_RESIZE. The resize or migration of a server failed for some reason. The destination server is being cleaned up and the original source server is restarting. SOFT_DELETED. The server is marked as deleted but the disk images are still available to restore. STOPPED. The server is powered off and the disk image still persists. SUSPENDED. The server is suspended, either by request or necessity. This status appears for only the XenServer/XCP, KVM, and ESXi hypervisors. Administrative users can suspend an instance if it is infrequently used or to perform system maintenance. When you suspend an instance, its VM state is stored on disk, all memory is written to disk, and the virtual machine is stopped. Suspending an instance is similar to placing a device in hibernation; memory and vCPUs become available to create other instances. VERIFY_RESIZE. System is awaiting confirmation that the server is operational after a move or resize. SHUTOFF. The server was powered down by the user, either through the OpenStack Compute API or from within the server. For example, the user issued a shutdown -h command from within the server. If the OpenStack Compute manager detects that the VM was powered down, it transitions the server to the SHUTOFF status. """ server = self.api.getServer(machineId) if server['status'] in ('ERROR', 'DELETED'): logger.warning('Got server status %s for %s: %s', server['status'], machineId, server.get('fault')) return server['status'] def startMachine(self, machineId: str) -> None: """ Tries to start a machine. No check is done, it is simply requested to OpenStack. This start also "resume" suspended/paused machines Args: machineId: Id of the machine Returns: """ self.api.startServer(machineId) def stopMachine(self, machineId: str) -> None: """ Tries to stop a machine. No check is done, it is simply requested to OpenStack Args: machineId: Id of the machine Returns: """ self.api.stopServer(machineId) def resetMachine(self, machineId: str) -> None: """ Tries to stop a machine. No check is done, it is simply requested to OpenStack Args: machineId: Id of the machine Returns: """ self.api.resetServer(machineId) def suspendMachine(self, machineId: str) -> None: """ Tries to suspend a machine. No check is done, it is simply requested to OpenStack Args: machineId: Id of the machine Returns: """ self.api.suspendServer(machineId) def resumeMachine(self, machineId: str) -> None: """ Tries to start a machine. No check is done, it is simply requested to OpenStack Args: machineId: Id of the machine Returns: """ self.api.resumeServer(machineId) def removeMachine(self, machineId: str) -> None: """ Tries to delete a machine. No check is done, it is simply requested to OpenStack Args: machineId: Id of the machine Returns: """ self.api.deleteServer(machineId) def getNetInfo(self, machineId: str) -> typing.Tuple[str, str]: """ Gets the mac address of first nic of the machine """ net = self.api.getServer(machineId)['addresses'] vals = next( iter(net.values()) )[0] # Returns "any" mac address of any interface. We just need only one interface info # vals = six.next(six.itervalues(net))[0] return vals['OS-EXT-IPS-MAC:mac_addr'].upper(), vals['addr'] def getBaseName(self) -> str: """ Returns the base name """ return self.baseName.value def getLenName(self) -> int: """ Returns the length of numbers part """ return int(self.lenName.value)
class TRDPTransport(BaseRDPTransport): """ Provides access via RDP to service. This transport can use an domain. If username processed by authenticator contains '@', it will split it and left-@-part will be username, and right password """ typeName = _('RDP') typeType = 'TSRDPTransport' typeDescription = _('RDP Protocol. Tunneled connection.') group = transports.TUNNELED_GROUP tunnelServer = gui.TextField( label=_('Tunnel server'), order=1, tooltip= _('IP or Hostname of tunnel server sent to client device ("public" ip) and port. (use HOST:PORT format)' ), tab=gui.TUNNEL_TAB, ) tunnelWait = gui.NumericField( length=3, label=_('Tunnel wait time'), defvalue='60', minValue=5, maxValue=65536, order=2, tooltip=_('Maximum time to wait before closing the tunnel listener'), required=True, tab=gui.TUNNEL_TAB, ) verifyCertificate = gui.CheckBoxField( label=_('Force SSL certificate verification'), order=23, tooltip= _('If enabled, the certificate of tunnel server will be verified (recommended).' ), defvalue=gui.TRUE, tab=gui.TUNNEL_TAB, ) useEmptyCreds = BaseRDPTransport.useEmptyCreds fixedName = BaseRDPTransport.fixedName fixedPassword = BaseRDPTransport.fixedPassword withoutDomain = BaseRDPTransport.withoutDomain fixedDomain = BaseRDPTransport.fixedDomain allowSmartcards = BaseRDPTransport.allowSmartcards allowPrinters = BaseRDPTransport.allowPrinters allowDrives = BaseRDPTransport.allowDrives enforceDrives = BaseRDPTransport.enforceDrives allowSerials = BaseRDPTransport.allowSerials allowClipboard = BaseRDPTransport.allowClipboard allowAudio = BaseRDPTransport.allowAudio allowWebcam = BaseRDPTransport.allowWebcam wallpaper = BaseRDPTransport.wallpaper multimon = BaseRDPTransport.multimon aero = BaseRDPTransport.aero smooth = BaseRDPTransport.smooth showConnectionBar = BaseRDPTransport.showConnectionBar credssp = BaseRDPTransport.credssp screenSize = BaseRDPTransport.screenSize colorDepth = BaseRDPTransport.colorDepth alsa = BaseRDPTransport.alsa multimedia = BaseRDPTransport.multimedia printerString = BaseRDPTransport.printerString smartcardString = BaseRDPTransport.smartcardString customParameters = BaseRDPTransport.customParameters allowMacMSRDC = BaseRDPTransport.allowMacMSRDC customParametersMAC = BaseRDPTransport.customParametersMAC def initialize(self, values: 'Module.ValuesType'): if values: if values['tunnelServer'].count(':') != 1: raise transports.Transport.ValidationException( _('Must use HOST:PORT in Tunnel Server Field')) def getUDSTransportScript( # pylint: disable=too-many-locals self, userService: 'models.UserService', transport: 'models.Transport', ip: str, os: typing.Dict[str, str], user: '******', password: str, request: 'HttpRequest', ) -> typing.Tuple[str, str, typing.Mapping[str, typing.Any]]: # We use helper to keep this clean # prefs = user.prefs('rdp') ci = self.getConnectionInfo(userService, user, password) username, password, domain = ci['username'], ci['password'], ci[ 'domain'] # escape conflicting chars : Note, on 3.0 this should not be neccesary. Kept until more tests # password = password.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'") # width, height = CommonPrefs.getWidthHeight(prefs) # depth = CommonPrefs.getDepth(prefs) width, height = self.screenSize.value.split('x') depth = self.colorDepth.value ticket = TicketStore.create_for_tunnel( userService=userService, port=3389, validity=self.tunnelWait.num() + 60, # Ticket overtime ) tunHost, tunPort = self.tunnelServer.value.split(':') r = RDPFile(width == '-1' or height == '-1', width, height, depth, target=os['OS']) r.enablecredsspsupport = ci.get( 'sso') == 'True' or self.credssp.isTrue() r.address = '{address}' r.username = username r.password = password r.domain = domain r.redirectPrinters = self.allowPrinters.isTrue() r.redirectSmartcards = self.allowSmartcards.isTrue() r.redirectDrives = self.allowDrives.value r.redirectSerials = self.allowSerials.isTrue() r.enableClipboard = self.allowClipboard.isTrue() r.redirectAudio = self.allowAudio.isTrue() r.redirectWebcam = self.allowWebcam.isTrue() r.showWallpaper = self.wallpaper.isTrue() r.multimon = self.multimon.isTrue() r.desktopComposition = self.aero.isTrue() r.smoothFonts = self.smooth.isTrue() r.enablecredsspsupport = self.credssp.isTrue() r.multimedia = self.multimedia.isTrue() r.alsa = self.alsa.isTrue() r.smartcardString = self.smartcardString.value r.printerString = self.printerString.value r.linuxCustomParameters = self.customParameters.value r.enforcedShares = self.enforceDrives.value osName = { OsDetector.Windows: 'windows', OsDetector.Linux: 'linux', OsDetector.Macintosh: 'macosx', }.get(os['OS']) if osName is None: return super().getUDSTransportScript(userService, transport, ip, os, user, password, request) sp: typing.MutableMapping[str, typing.Any] = { 'tunHost': tunHost, 'tunPort': tunPort, 'tunWait': self.tunnelWait.num(), 'tunChk': self.verifyCertificate.isTrue(), 'ticket': ticket, 'password': password, 'this_server': request.build_absolute_uri('/'), } if osName == 'windows': if password != '': r.password = '******' sp.update({ 'as_file': r.as_file, }) elif osName == 'linux': sp.update({ 'as_new_xfreerdp_params': r.as_new_xfreerdp_params, }) else: # Mac r.linuxCustomParameters = self.customParametersMAC.value sp.update({ 'as_new_xfreerdp_params': r.as_new_xfreerdp_params, 'as_file': r.as_file if self.allowMacMSRDC.isTrue() else '', 'as_rdp_url': r.as_rdp_url if self.allowMacMSRDC.isTrue() else '', }) return self.getScript('scripts/{}/tunnel.py', osName, sp)
class BaseSpiceTransport(transports.Transport): """ Provides access via SPICE to service. This transport can use an domain. If username processed by authenticator contains '@', it will split it and left-@-part will be username, and right password """ iconFile = 'spice.png' protocol = protocols.SPICE useEmptyCreds = gui.CheckBoxField( order=1, label=_('Empty credentials'), tooltip=_('If checked, the credentials used to connect will be emtpy'), tab=gui.CREDENTIALS_TAB) fixedName = gui.TextField( order=2, label=_('Username'), tooltip=_( 'If not empty, this username will be always used as credential'), tab=gui.CREDENTIALS_TAB) fixedPassword = gui.PasswordField( order=3, label=_('Password'), tooltip=_( 'If not empty, this password will be always used as credential'), tab=gui.CREDENTIALS_TAB) serverCertificate = gui.TextField( order=4, length=4096, multiline=4, label=_('Certificate'), tooltip= _('Server certificate (public), can be found on your ovirt engine, probably at /etc/pki/ovirt-engine/certs/ca.der (Use the contents of this file).' ), required=False) fullScreen = gui.CheckBoxField( order=5, label=_('Fullscreen Mode'), tooltip=_('If checked, viewer will be shown on fullscreen mode-'), tab=gui.ADVANCED_TAB) smartCardRedirect = gui.CheckBoxField( order=6, label=_('Smartcard Redirect'), tooltip=_( 'If checked, SPICE protocol will allow smartcard redirection.'), defvalue=gui.FALSE, tab=gui.ADVANCED_TAB) usbShare = gui.CheckBoxField( order=7, label=_('Enable USB'), tooltip=_('If checked, USB redirection will be allowed.'), defvalue=gui.FALSE, tab=gui.ADVANCED_TAB) autoNewUsbShare = gui.CheckBoxField( order=8, label=_('New USB Auto Sharing'), tooltip=_('Auto-redirect USB devices when plugged in.'), defvalue=gui.FALSE, tab=gui.ADVANCED_TAB) def isAvailableFor(self, userService: 'models.UserService', ip: str) -> bool: """ Checks if the transport is available for the requested destination ip """ ready = self.cache.get(ip) if ready is None: userServiceInstance: typing.Any = userService.getInstance( ) # Disable mypy checks on this con = userServiceInstance.getConsoleConnection() logger.debug('Connection data: %s', con) if con is None: return False port, secure_port = con['port'] or -1, con['secure_port'] or -1 # test ANY of the ports port_to_test = port if port != -1 else secure_port if port_to_test == -1: self.cache.put( 'cachedMsg', 'Could not find the PORT for connection', 120) # Write a message, that will be used from getCustom logger.info('SPICE didn\'t find has any port: %s', con) return False self.cache.put( 'cachedMsg', 'Could not reach server "{}" on port "{}" from broker (prob. causes are name resolution & firewall rules)' .format(con['address'], port_to_test), 120) if self.testServer(userService, con['address'], port_to_test) is True: self.cache.put(ip, 'Y', READY_CACHE_TIMEOUT) ready = 'Y' return ready == 'Y' def getCustomAvailableErrorMsg(self, userService: 'models.UserService', ip: str) -> str: msg = self.cache.get('cachedMsg') if msg is None: return transports.Transport.getCustomAvailableErrorMsg( self, userService, ip) return msg def processedUser(self, userService: 'models.UserService', user: '******') -> str: v = self.processUserPassword(userService, user, '') return v['username'] def processUserPassword(self, userService: 'models.UserService', user: '******', password: str) -> typing.Dict[str, str]: username = user.getUsernameForAuth() if self.fixedName.value: username = self.fixedName.value if self.fixedPassword.value: password = self.fixedPassword.value if self.useEmptyCreds.isTrue(): username, password = '', '' # Fix username/password acording to os manager username, password = userService.processUserPassword( username, password) return { 'protocol': self.protocol, 'username': username, 'password': password } def getConnectionInfo(self, userService: typing.Union['models.UserService', 'models.ServicePool'], user: '******', password: str) -> typing.Dict[str, str]: return self.processUserPassword(userService, user, password) def getScript( self, scriptNameTemplate: str, osName: str, params: typing.Dict[str, typing.Any] ) -> typing.Tuple[str, str, typing.Dict[str, typing.Any]]: # Reads script scriptNameTemplate = scriptNameTemplate.format(osName) with open(os.path.join(os.path.dirname(__file__), scriptNameTemplate)) as f: script = f.read() # Reads signature with open( os.path.join(os.path.dirname(__file__), scriptNameTemplate + '.signature')) as f: signature = f.read() return script, signature, params
class OVirtProvider(services.ServiceProvider): # pylint: disable=too-many-public-methods """ This class represents the sample services provider In this class we provide: * The Provider functionality * The basic configuration parameters for the provider * The form fields needed by administrators to configure this provider :note: At class level, the translation must be simply marked as so using ugettext_noop. This is so cause we will translate the string when sent to the administration client. For this class to get visible at administration client as a provider type, we MUST register it at package __init__. """ # : What kind of services we offer, this are classes inherited from Service offers = [OVirtLinkedService] # : Name to show the administrator. This string will be translated BEFORE # : sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) typeName = _('oVirt/RHEV Platform Provider') # : Type used internally to identify this provider typeType = 'oVirtPlatform' # : Description shown at administration interface for this provider typeDescription = _('oVirt platform service provider') # : Icon file used as icon for this provider. This string will be translated # : BEFORE sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) iconFile = 'provider.png' # now comes the form fields # There is always two fields that are requested to the admin, that are: # Service Name, that is a name that the admin uses to name this provider # Description, that is a short description that the admin gives to this provider # Now we are going to add a few fields that we need to use this provider # Remember that these are "dummy" fields, that in fact are not required # but used for sample purposes # If we don't indicate an order, the output order of fields will be # "random" ovirtVersion = gui.ChoiceField( order=1, label=_('oVirt Version'), tooltip=_('oVirt Server Version'), # In this case, the choice can have none value selected by default required=True, rdonly=False, values=[ gui.choiceItem('4', '4.x'), ], defvalue='4' # Default value is the ID of the choicefield ) host = gui.TextField(length=64, label=_('Host'), order=2, tooltip=_('oVirt Server IP or Hostname'), required=True) username = gui.TextField( length=32, label=_('Username'), order=3, tooltip=_( 'User with valid privileges on oVirt, (use "user@domain" form)'), required=True, defvalue='admin@internal') password = gui.PasswordField(lenth=32, label=_('Password'), order=4, tooltip=_('Password of the user of oVirt'), required=True) maxPreparingServices = gui.NumericField( length=3, label=_('Creation concurrency'), defvalue='10', minValue=1, maxValue=65536, order=50, tooltip=_('Maximum number of concurrently creating VMs'), required=True, tab=gui.ADVANCED_TAB) maxRemovingServices = gui.NumericField( length=3, label=_('Removal concurrency'), defvalue='5', minValue=1, maxValue=65536, order=51, tooltip=_('Maximum number of concurrently removing VMs'), required=True, tab=gui.ADVANCED_TAB) timeout = gui.NumericField( length=3, label=_('Timeout'), defvalue='10', order=90, tooltip=_('Timeout in seconds of connection to oVirt'), required=True, tab=gui.ADVANCED_TAB) macsRange = gui.TextField( length=36, label=_('Macs range'), defvalue='52:54:00:00:00:00-52:54:00:FF:FF:FF', order=91, rdonly=True, tooltip=_('Range of valid macs for UDS managed machines'), required=True, tab=gui.ADVANCED_TAB) # Own variables _api: typing.Optional[client.Client] = None # oVirt engine, right now, only permits a connection to one server and only one per instance # If we want to connect to more than one server, we need keep locked access to api, change api server, etc.. # We have implemented an "exclusive access" client that will only connect to one server at a time (using locks) # and this way all will be fine def __getApi(self) -> client.Client: """ Returns the connection API object for oVirt (using ovirtsdk) """ if self._api is None: self._api = client.Client(self.host.value, self.username.value, self.password.value, self.timeout.value, self.cache) return self._api # There is more fields type, but not here the best place to cover it def initialize(self, values: 'Module.ValuesType') -> None: """ We will use the "autosave" feature for form fields """ # Just reset _api connection variable self._api = None if values is not None: self.macsRange.value = validators.validateMacRange( self.macsRange.value) self.timeout.value = validators.validateTimeout(self.timeout.value) logger.debug(self.host.value) def testConnection(self) -> bool: """ Test that conection to oVirt server is fine Returns True if all went fine, false if id didn't """ return self.__getApi().test() def testValidVersion(self): """ Checks that this version of ovirt if "fully functional" and does not needs "patchs' """ return list(self.__getApi().isFullyFunctionalVersion()) def getMachines( self, force: bool = False ) -> typing.List[typing.MutableMapping[str, typing.Any]]: """ Obtains the list of machines inside oVirt. Machines starting with UDS are filtered out Args: force: If true, force to update the cache, if false, tries to first get data from cache and, if valid, return this. Returns An array of dictionaries, containing: 'name' 'id' 'cluster_id' """ return self.__getApi().getVms(force) def getClusters( self, force: bool = False ) -> typing.List[typing.MutableMapping[str, typing.Any]]: """ Obtains the list of clusters inside oVirt. Args: force: If true, force to update the cache, if false, tries to first get data from cache and, if valid, return this. Returns Filters out clusters not attached to any datacenter An array of dictionaries, containing: 'name' 'id' 'datacenter_id' """ return self.__getApi().getClusters(force) def getClusterInfo( self, clusterId: str, force: bool = False) -> typing.MutableMapping[str, typing.Any]: """ Obtains the cluster info Args: datacenterId: Id of the cluster to get information about it force: If true, force to update the cache, if false, tries to first get data from cache and, if valid, return this. Returns A dictionary with following values 'name' 'id' 'datacenter_id' """ return self.__getApi().getClusterInfo(clusterId, force) def getDatacenterInfo( self, datacenterId: str, force: bool = False) -> typing.MutableMapping[str, typing.Any]: """ Obtains the datacenter info Args: datacenterId: Id of the datacenter to get information about it force: If true, force to update the cache, if false, tries to first get data from cache and, if valid, return this. Returns A dictionary with following values 'name' 'id' 'storage_type' -> ('isisi', 'nfs', ....) 'storage_format' -> ('v1', v2') 'description' 'storage' -> array of dictionaries, with: 'id' -> Storage id 'name' -> Storage name 'type' -> Storage type ('data', 'iso') 'available' -> Space available, in bytes 'used' -> Space used, in bytes 'active' -> True or False """ return self.__getApi().getDatacenterInfo(datacenterId, force) def getStorageInfo( self, storageId: str, force: bool = False) -> typing.MutableMapping[str, typing.Any]: """ Obtains the storage info Args: storageId: Id of the storage to get information about it force: If true, force to update the cache, if false, tries to first get data from cache and, if valid, return this. Returns A dictionary with following values 'id' -> Storage id 'name' -> Storage name 'type' -> Storage type ('data', 'iso') 'available' -> Space available, in bytes 'used' -> Space used, in bytes # 'active' -> True or False --> This is not provided by api?? (api.storagedomains.get) """ return self.__getApi().getStorageInfo(storageId, force) def makeTemplate(self, name: str, comments: str, machineId: str, clusterId: str, storageId: str, displayType: str) -> str: """ Publish the machine (makes a template from it so we can create COWs) and returns the template id of the creating machine Args: name: Name of the machine (care, only ascii characters and no spaces!!!) machineId: id of the machine to be published clusterId: id of the cluster that will hold the machine storageId: id of the storage tuat will contain the publication AND linked clones displayType: type of display (for oVirt admin interface only) Returns Raises an exception if operation could not be acomplished, or returns the id of the template being created. """ return self.__getApi().makeTemplate(name, comments, machineId, clusterId, storageId, displayType) def getTemplateState(self, templateId: str) -> str: """ Returns current template state. Returned values could be: ok locked removed (don't know if ovirt returns something more right now, will test what happens when template can't be published) """ return self.__getApi().getTemplateState(templateId) def getMachineState(self, machineId: str) -> str: """ Returns the state of the machine This method do not uses cache at all (it always tries to get machine state from oVirt server) Args: machineId: Id of the machine to get state Returns: one of this values: unassigned, down, up, powering_up, powered_down, paused, migrating_from, migrating_to, unknown, not_responding, wait_for_launch, reboot_in_progress, saving_state, restoring_state, suspended, image_illegal, image_locked or powering_down Also can return'unknown' if Machine is not known """ return self.__getApi().getMachineState(machineId) def removeTemplate(self, templateId: str) -> None: """ Removes a template from ovirt server Returns nothing, and raises an Exception if it fails """ return self.__getApi().removeTemplate(templateId) def deployFromTemplate(self, name: str, comments: str, templateId: str, clusterId: str, displayType: str, usbType: str, memoryMB: int, guaranteedMB: int) -> str: """ Deploys a virtual machine on selected cluster from selected template Args: name: Name (sanitized) of the machine comments: Comments for machine templateId: Id of the template to deploy from clusterId: Id of the cluster to deploy to displayType: 'vnc' or 'spice'. Display to use ad oVirt admin interface memoryMB: Memory requested for machine, in MB guaranteedMB: Minimum memory guaranteed for this machine Returns: Id of the machine being created form template """ return self.__getApi().deployFromTemplate(name, comments, templateId, clusterId, displayType, usbType, memoryMB, guaranteedMB) def startMachine(self, machineId: str) -> None: """ Tries to start a machine. No check is done, it is simply requested to oVirt. This start also "resume" suspended/paused machines Args: machineId: Id of the machine Returns: """ self.__getApi().startMachine(machineId) def stopMachine(self, machineId: str) -> None: """ Tries to start a machine. No check is done, it is simply requested to oVirt Args: machineId: Id of the machine Returns: """ self.__getApi().stopMachine(machineId) def suspendMachine(self, machineId: str) -> None: """ Tries to start a machine. No check is done, it is simply requested to oVirt Args: machineId: Id of the machine Returns: """ self.__getApi().suspendMachine(machineId) def removeMachine(self, machineId: str) -> None: """ Tries to delete a machine. No check is done, it is simply requested to oVirt Args: machineId: Id of the machine Returns: """ self.__getApi().removeMachine(machineId) def updateMachineMac(self, machineId: str, macAddres: str) -> None: """ Changes the mac address of first nic of the machine to the one specified """ self.__getApi().updateMachineMac(machineId, macAddres) def fixUsb(self, machineId: str) -> None: self.__getApi().fixUsb(machineId) def getMacRange(self) -> str: return self.macsRange.value def getConsoleConnection( self, machineId: str ) -> typing.Optional[typing.MutableMapping[str, typing.Any]]: return self.__getApi().getConsoleConnection(machineId) @staticmethod def test(env: 'Environment', data: 'Module.ValuesType') -> typing.List[typing.Any]: """ Test ovirt Connectivity Args: env: environment passed for testing (temporal environment passed) data: data passed for testing (data obtained from the form definition) Returns: Array of two elements, first is True of False, depending on test (True is all right, false is error), second is an String with error, preferably internacionalizated.. """ # try: # # We instantiate the provider, but this may fail... # instance = Provider(env, data) # logger.debug('Methuselah has {0} years and is {1} :-)' # .format(instance.methAge.value, instance.methAlive.value)) # except ServiceProvider.ValidationException as e: # # If we say that meth is alive, instantiation will # return [False, str(e)] # except Exception as e: # logger.exception("Exception caugth!!!") # return [False, str(e)] # return [True, _('Nothing tested, but all went fine..')] ov = OVirtProvider(env, data) if ov.testConnection() is True: return ov.testValidVersion() return [False, _("Connection failed. Check connection params")]
class Provider(ServiceProvider): ''' This class represents the sample services provider In this class we provide: * The Provider functionality * The basic configuration parameters for the provider * The form fields needed by administrators to configure this provider :note: At class level, the translation must be simply marked as so using ugettext_noop. This is so cause we will translate the string when sent to the administration client. For this class to get visible at administration client as a provider type, we MUST register it at package __init__. ''' # : What kind of services we offer, this are classes inherited from Service offers = [XenLinkedService] # : Name to show the administrator. This string will be translated BEFORE # : sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) typeName = _('Xenserver Platform Provider') # : Type used internally to identify this provider typeType = 'XenPlatform' # : Description shown at administration interface for this provider typeDescription = _('XenServer platform service provider') # : Icon file used as icon for this provider. This string will be translated # : BEFORE sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) iconFile = 'provider.png' # now comes the form fields # There is always two fields that are requested to the admin, that are: # Service Name, that is a name that the admin uses to name this provider # Description, that is a short description that the admin gives to this provider # Now we are going to add a few fields that we need to use this provider # Remember that these are "dummy" fields, that in fact are not required # but used for sample purposes # If we don't indicate an order, the output order of fields will be # "random" host = gui.TextField(length=64, label=_('Host'), order=1, tooltip=_('XenServer Server IP or Hostname'), required=True) username = gui.TextField(length=32, label=_('Username'), order=2, tooltip=_('User with valid privileges on XenServer'), required=True, defvalue='root') password = gui.PasswordField(lenth=32, label=_('Password'), order=3, tooltip=_('Password of the user of XenServer'), required=True) maxPreparingServices = gui.NumericField(length=3, label=_('Creation concurrency'), defvalue='10', minValue=1, maxValue=65536, order=50, tooltip=_('Maximum number of concurrently creating VMs'), required=True, tab=gui.ADVANCED_TAB) maxRemovingServices = gui.NumericField(length=3, label=_('Removal concurrency'), defvalue='5', minValue=1, maxValue=65536, order=51, tooltip=_('Maximum number of concurrently removing VMs'), required=True, tab=gui.ADVANCED_TAB) macsRange = gui.TextField(length=36, label=_('Macs range'), defvalue='02:46:00:00:00:00-02:46:00:FF:FF:FF', order=90, rdonly=True, tooltip=_('Range of valid macs for created machines'), required=True, tab=gui.ADVANCED_TAB) verifySSL = gui.CheckBoxField(label=_('Verify Certificate'), order=91, tooltip=_('If selected, certificate will be checked against system valid certificate providers'), required=True, tab=gui.ADVANCED_TAB) # XenServer engine, right now, only permits a connection to one server and only one per instance # If we want to connect to more than one server, we need keep locked access to api, change api server, etc.. # We have implemented an "exclusive access" client that will only connect to one server at a time (using locks) # and this way all will be fine def __getApi(self, force=False): ''' Returns the connection API object for XenServer (using XenServersdk) ''' logger.debug('API verifySSL: {} {}'.format(self.verifySSL.value, self.verifySSL.isTrue())) if self._api is None or force: self._api = XenServer(self.host.value, '443', self.username.value, self.password.value, True, self.verifySSL.isTrue()) return self._api # There is more fields type, but not here the best place to cover it def initialize(self, values=None): ''' We will use the "autosave" feature for form fields ''' # Just reset _api connection variable self._api = None def testConnection(self): ''' Test that conection to XenServer server is fine Returns True if all went fine, false if id didn't ''' self.__getApi().test() def checkTaskFinished(self, task): ''' Checks a task state. Returns None if task is Finished Returns a number indicating % of completion if running Raises an exception with status else ('cancelled', 'unknown', 'failure') ''' if task is None or task == '': return (True, '') ts = self.__getApi().getTaskInfo(task) logger.debug('Task status: {0}'.format(ts)) if ts['status'] == 'running': return (False, ts['progress']) if ts['status'] == 'success': return (True, ts['result']) # Any other state, raises an exception raise Exception(six.text_type(ts['result'])) # Should be error message def getMachines(self, force=False): ''' Obtains the list of machines inside XenServer. Machines starting with UDS are filtered out Args: force: If true, force to update the cache, if false, tries to first get data from cache and, if valid, return this. Returns An array of dictionaries, containing: 'name' 'id' 'cluster_id' ''' for m in self.__getApi().getVMs(): if m['name'][:3] == 'UDS': continue yield m def getStorages(self, force=False): ''' Obtains the list of storages inside XenServer. Args: force: If true, force to update the cache, if false, tries to first get data from cache and, if valid, return this. Returns An array of dictionaries, containing: 'name' 'id' 'size' 'used' ''' return self.__getApi().getSRs() def getStorageInfo(self, storageId, force=False): ''' Obtains the storage info Args: storageId: Id of the storage to get information about it force: If true, force to update the cache, if false, tries to first get data from cache and, if valid, return this. Returns A dictionary with following values 'id' -> Storage id 'name' -> Storage name 'type' -> Storage type ('data', 'iso') 'available' -> Space available, in bytes 'used' -> Space used, in bytes # 'active' -> True or False --> This is not provided by api?? (api.storagedomains.get) ''' return self.__getApi().getSRInfo(storageId) def getNetworks(self, force=False): return self.__getApi().getNetworks() def cloneForTemplate(self, name, comments, machineId, sr): task = self.__getApi().cloneVM(machineId, name, sr) logger.debug('Task for cloneForTemplate: {0}'.format(task)) return task def convertToTemplate(self, machineId, shadowMultiplier=4): ''' Publish the machine (makes a template from it so we can create COWs) and returns the template id of the creating machine Args: name: Name of the machine (care, only ascii characters and no spaces!!!) machineId: id of the machine to be published clusterId: id of the cluster that will hold the machine storageId: id of the storage tuat will contain the publication AND linked clones displayType: type of display (for XenServer admin interface only) Returns Raises an exception if operation could not be acomplished, or returns the id of the template being created. ''' self.__getApi().convertToTemplate(machineId, shadowMultiplier) def getMachineState(self, machineId): ''' Returns the state of the machine This method do not uses cache at all (it always tries to get machine state from XenServer server) Args: machineId: Id of the machine to get state Returns: one of this values: unassigned, down, up, powering_up, powered_down, paused, migrating_from, migrating_to, unknown, not_responding, wait_for_launch, reboot_in_progress, saving_state, restoring_state, suspended, image_illegal, image_locked or powering_down Also can return'unknown' if Machine is not known ''' return self.__getApi().getMachineState(machineId) return State.INACTIVE def removeTemplate(self, templateId): ''' Removes a template from XenServer server Returns nothing, and raises an Exception if it fails ''' return self.__getApi().removeTemplate(templateId) def startDeployFromTemplate(self, name, comments, templateId): ''' Deploys a virtual machine on selected cluster from selected template Args: name: Name (sanitized) of the machine comments: Comments for machine templateId: Id of the template to deploy from clusterId: Id of the cluster to deploy to displayType: 'vnc' or 'spice'. Display to use ad XenServer admin interface memoryMB: Memory requested for machine, in MB guaranteedMB: Minimum memory guaranteed for this machine Returns: Id of the machine being created form template ''' return self.__getApi().cloneTemplate(templateId, name) def getVMPowerState(self, machineId): ''' Returns current machine power state ''' return self.__getApi().getVMPowerState(machineId) def startVM(self, machineId, async=True): ''' Tries to start a machine. No check is done, it is simply requested to XenServer. This start also "resume" suspended/paused machines Args: machineId: Id of the machine Returns: ''' return self.__getApi().startVM(machineId, async)
class TRDPTransport(BaseRDPTransport): ''' Provides access via RDP to service. This transport can use an domain. If username processed by authenticator contains '@', it will split it and left-@-part will be username, and right password ''' typeName = _('RDP') typeType = 'TSRDPTransport' typeDescription = _('RDP Protocol. Tunneled connection.') needsJava = True # If this transport needs java for rendering protocol = transports.protocols.RDP group = transports.TUNNELED_GROUP tunnelServer = gui.TextField( label=_('Tunnel server'), order=1, tooltip= _('IP or Hostname of tunnel server sent to client device ("public" ip) and port. (use HOST:PORT format)' ), tab=gui.TUNNEL_TAB) # tunnelCheckServer = gui.TextField(label=_('Tunnel host check'), order=2, tooltip=_('If not empty, this server will be used to check if service is running before assigning it to user. (use HOST:PORT format)'), tab=gui.TUNNEL_TAB) tunnelWait = gui.NumericField( length=3, label=_('Tunnel wait time'), defvalue='10', minValue=1, maxValue=65536, order=2, tooltip=_('Maximum time to wait before closing the tunnel listener'), required=True, tab=gui.TUNNEL_TAB) useEmptyCreds = BaseRDPTransport.useEmptyCreds fixedName = BaseRDPTransport.fixedName fixedPassword = BaseRDPTransport.fixedPassword withoutDomain = BaseRDPTransport.withoutDomain fixedDomain = BaseRDPTransport.fixedDomain allowSmartcards = BaseRDPTransport.allowSmartcards allowPrinters = BaseRDPTransport.allowPrinters allowDrives = BaseRDPTransport.allowDrives enforceDrives = BaseRDPTransport.enforceDrives allowSerials = BaseRDPTransport.allowSerials allowClipboard = BaseRDPTransport.allowClipboard allowAudio = BaseRDPTransport.allowAudio wallpaper = BaseRDPTransport.wallpaper multimon = BaseRDPTransport.multimon aero = BaseRDPTransport.aero smooth = BaseRDPTransport.smooth showConnectionBar = BaseRDPTransport.showConnectionBar credssp = BaseRDPTransport.credssp screenSize = BaseRDPTransport.screenSize colorDepth = BaseRDPTransport.colorDepth alsa = BaseRDPTransport.alsa multimedia = BaseRDPTransport.multimedia redirectHome = BaseRDPTransport.redirectHome printerString = BaseRDPTransport.printerString smartcardString = BaseRDPTransport.smartcardString customParameters = BaseRDPTransport.customParameters def initialize(self, values: 'Module.ValuesType'): if values: if values['tunnelServer'].count(':') != 1: raise transports.Transport.ValidationException( _('Must use HOST:PORT in Tunnel Server Field')) def getUDSTransportScript( # pylint: disable=too-many-locals self, userService: 'models.UserService', transport: 'models.Transport', ip: str, os: typing.Dict[str, str], user: '******', password: str, request: 'HttpRequest' ) -> typing.Tuple[str, str, typing.Dict[str, typing.Any]]: # We use helper to keep this clean # prefs = user.prefs('rdp') ci = self.getConnectionInfo(userService, user, password) username, password, domain = ci['username'], ci['password'], ci[ 'domain'] # width, height = CommonPrefs.getWidthHeight(prefs) # depth = CommonPrefs.getDepth(prefs) width, height = self.screenSize.value.split('x') depth = self.colorDepth.value tunpass = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _i in range(12)) tunuser = TicketStore.create(tunpass) sshHost, sshPort = self.tunnelServer.value.split(':') logger.debug('Username generated: %s, password: %s', tunuser, tunpass) r = RDPFile(width == '-1' or height == '-1', width, height, depth, target=os['OS']) r.enablecredsspsupport = ci.get( 'sso') == 'True' or self.credssp.isTrue() r.address = '{address}' r.username = username r.password = password r.domain = domain r.redirectPrinters = self.allowPrinters.isTrue() r.redirectSmartcards = self.allowSmartcards.isTrue() r.redirectDrives = self.allowDrives.value r.redirectHome = self.redirectHome.isTrue() r.redirectSerials = self.allowSerials.isTrue() r.enableClipboard = self.allowClipboard.isTrue() r.redirectAudio = self.allowAudio.isTrue() r.showWallpaper = self.wallpaper.isTrue() r.multimon = self.multimon.isTrue() r.desktopComposition = self.aero.isTrue() r.smoothFonts = self.smooth.isTrue() r.enablecredsspsupport = self.credssp.isTrue() r.multimedia = self.multimedia.isTrue() r.alsa = self.alsa.isTrue() r.smartcardString = self.smartcardString.value r.printerString = self.printerString.value r.linuxCustomParameters = self.customParameters.value r.enforcedShares = self.enforceDrives.value osName = { OsDetector.Windows: 'windows', OsDetector.Linux: 'linux', OsDetector.Macintosh: 'macosx' }.get(os['OS']) if osName is None: return super().getUDSTransportScript(userService, transport, ip, os, user, password, request) sp = { 'tunUser': tunuser, 'tunPass': tunpass, 'tunHost': sshHost, 'tunPort': sshPort, 'tunWait': self.tunnelWait.num(), 'ip': ip, 'password': password, 'this_server': request.build_absolute_uri('/'), } if osName == 'windows': if password != '': r.password = '******' sp.update({ 'as_file': r.as_file, }) elif osName == 'linux': sp.update({ 'as_new_xfreerdp_params': r.as_new_xfreerdp_params, 'as_rdesktop_params': r.as_rdesktop_params, }) else: # Mac sp.update({ 'as_file': r.as_file, 'as_cord_url': r.as_cord_url, 'as_new_xfreerdp_params': r.as_new_xfreerdp_params, }) if domain != '': sp['usernameWithDomain'] = '{}\\\\{}'.format(domain, username) else: sp['usernameWithDomain'] = username return self.getScript('scripts/{}/tunnel.py', osName, sp)
class BaseRDPTransport(transports.Transport): """ Provides access via RDP to service. This transport can use an domain. If username processed by authenticator contains '@', it will split it and left-@-part will be username, and right password """ iconFile = 'rdp.png' protocol = transports.protocols.RDP useEmptyCreds = gui.CheckBoxField(label=_('Empty creds'), order=11, tooltip=_('If checked, the credentials used to connect will be emtpy'), tab=gui.CREDENTIALS_TAB) fixedName = gui.TextField(label=_('Username'), order=12, tooltip=_('If not empty, this username will be always used as credential'), tab=gui.CREDENTIALS_TAB) fixedPassword = gui.PasswordField(label=_('Password'), order=13, tooltip=_('If not empty, this password will be always used as credential'), tab=gui.CREDENTIALS_TAB) withoutDomain = gui.CheckBoxField(label=_('Without Domain'), order=14, tooltip=_('If checked, the domain part will always be emptied (to connect to xrdp for example is needed)'), tab=gui.CREDENTIALS_TAB) fixedDomain = gui.TextField(label=_('Domain'), order=15, tooltip=_('If not empty, this domain will be always used as credential (used as DOMAIN\\user)'), tab=gui.CREDENTIALS_TAB) allowSmartcards = gui.CheckBoxField(label=_('Allow Smartcards'), order=20, tooltip=_('If checked, this transport will allow the use of smartcards'), tab=gui.PARAMETERS_TAB) allowPrinters = gui.CheckBoxField(label=_('Allow Printers'), order=21, tooltip=_('If checked, this transport will allow the use of user printers'), tab=gui.PARAMETERS_TAB) allowDrives = gui.ChoiceField( label=_('Local drives policy'), order=22, tooltip=_('Local drives redirection policy'), defvalue='false', values=[ {'id': 'false', 'text': 'Allow none'}, {'id': 'dynamic', 'text': 'Allow PnP drives'}, {'id': 'true', 'text': 'Allow any drive'}, ], tab=gui.PARAMETERS_TAB ) enforceDrives = gui.TextField( label=_('Force drives'), order=23, tooltip=_('Use comma separated values, for example "C:,D:". If drives policy is disallowed, this will be ignored'), tab=gui.PARAMETERS_TAB ) allowSerials = gui.CheckBoxField(label=_('Allow Serials'), order=24, tooltip=_('If checked, this transport will allow the use of user serial ports'), tab=gui.PARAMETERS_TAB) allowClipboard = gui.CheckBoxField(label=_('Enable clipboard'), order=25, tooltip=_('If checked, copy-paste functions will be allowed'), tab=gui.PARAMETERS_TAB, defvalue=gui.TRUE) allowAudio = gui.CheckBoxField(label=_('Enable sound'), order=26, tooltip=_('If checked, sound will be redirected.'), tab=gui.PARAMETERS_TAB, defvalue=gui.TRUE) credssp = gui.CheckBoxField(label=_('Credssp Support'), order=27, tooltip=_('If checked, will enable Credentials Provider Support)'), tab=gui.PARAMETERS_TAB, defvalue=gui.TRUE) screenSize = gui.ChoiceField( label=_('Screen Size'), order=30, tooltip=_('Screen size for this transport'), defvalue='-1x-1', values=[ {'id': '640x480', 'text': '640x480'}, {'id': '800x600', 'text': '800x600'}, {'id': '1024x768', 'text': '1024x768'}, {'id': '1366x768', 'text': '1366x768'}, {'id': '1920x1080', 'text': '1920x1080'}, {'id': '-1x-1', 'text': 'Full screen'}, ], tab=gui.DISPLAY_TAB ) colorDepth = gui.ChoiceField( label=_('Color depth'), order=31, tooltip=_('Color depth for this connection'), defvalue='24', values=[ {'id': '8', 'text': '8'}, {'id': '16', 'text': '16'}, {'id': '24', 'text': '24'}, {'id': '32', 'text': '32'}, ], tab=gui.DISPLAY_TAB ) wallpaper = gui.CheckBoxField(label=_('Wallpaper/theme'), order=32, tooltip=_('If checked, the wallpaper and themes will be shown on machine (better user experience, more bandwidth)'), tab=gui.DISPLAY_TAB) multimon = gui.CheckBoxField(label=_('Multiple monitors'), order=33, tooltip=_('If checked, all client monitors will be used for displaying (only works on windows clients)'), tab=gui.DISPLAY_TAB) aero = gui.CheckBoxField(label=_('Allow Desk.Comp.'), order=34, tooltip=_('If checked, desktop composition will be allowed'), tab=gui.DISPLAY_TAB) smooth = gui.CheckBoxField(label=_('Font Smoothing'), order=35, tooltip=_('If checked, fonts smoothing will be allowed'), tab=gui.DISPLAY_TAB) showConnectionBar = gui.CheckBoxField(label=_('Connection Bar'), order=36, tooltip=_('If checked, connection bar will be shown (only on Windows clients)'), tab=gui.DISPLAY_TAB, defvalue=gui.TRUE) multimedia = gui.CheckBoxField(label=_('Multimedia sync'), order=40, tooltip=_('If checked. Linux client will use multimedia parameter for xfreerdp'), tab='Linux Client') alsa = gui.CheckBoxField(label=_('Use Alsa'), order=41, tooltip=_('If checked, Linux client will try to use ALSA, otherwise Pulse will be used'), tab='Linux Client') redirectHome = gui.CheckBoxField(label=_('Redirect home folder'), order=42, tooltip=_('If checked, Linux client will try to redirect /home local folder'), tab='Linux Client', defvalue=gui.FALSE) printerString = gui.TextField(label=_('Printer string'), order=43, tooltip=_('If printer is checked, the printer string used with xfreerdp client'), tab='Linux Client') smartcardString = gui.TextField(label=_('Smartcard string'), order=44, tooltip=_('If smartcard is checked, the smartcard string used with xfreerdp client'), tab='Linux Client') customParameters = gui.TextField(label=_('Custom parameters'), order=45, tooltip=_('If not empty, extra parameter to include for Linux Client (for example /usb:id,dev:054c:0268, or aything compatible with your xfreerdp client)'), tab='Linux Client') def isAvailableFor(self, userService: 'models.UserService', ip: str) -> bool: """ Checks if the transport is available for the requested destination ip Override this in yours transports """ logger.debug('Checking availability for %s', ip) ready = self.cache.get(ip) if ready is None: # Check again for ready if self.testServer(userService, ip, '3389') is True: self.cache.put(ip, 'Y', READY_CACHE_TIMEOUT) return True else: self.cache.put(ip, 'N', READY_CACHE_TIMEOUT) return ready == 'Y' def processedUser(self, userService: 'models.UserService', user: '******') -> str: v = self.processUserPassword(userService, user, '') return v['username'] def processUserPassword(self, userService: 'models.UserService', user: '******', password: '******') -> typing.Dict[str, typing.Any]: username = user.getUsernameForAuth() if self.fixedName.value != '': username = self.fixedName.value proc = username.split('@') if len(proc) > 1: domain = proc[1] else: domain = '' username = proc[0] if self.fixedPassword.value != '': password = self.fixedPassword.value if self.fixedDomain.value != '': domain = self.fixedDomain.value if self.useEmptyCreds.isTrue(): username, password, domain = '', '', '' if self.withoutDomain.isTrue(): domain = '' if domain != '': # If has domain if '.' in domain: # Dotter domain form username = username + '@' + domain domain = '' else: # In case of a NETBIOS domain (not recomended), join it so processUserPassword can deal with it username = domain + '\\' + username domain = '' # Temporal "fix" to check if we do something on processUserPassword # Fix username/password acording to os manager username, password = userService.processUserPassword(username, password) # Recover domain name if needed if '\\' in username: username, domain = username.split('\\') return {'protocol': self.protocol, 'username': username, 'password': password, 'domain': domain} def getConnectionInfo( self, userService: typing.Union['models.UserService', 'models.ServicePool'], user: '******', password: str ) -> typing.Dict[str, str]: return self.processUserPassword(userService, user, password) def getScript(self, scriptNameTemplate: str, osName: str, params: typing.Dict[str, typing.Any]) -> typing.Tuple[str, str, typing.Dict[str, typing.Any]]: # Reads script scriptNameTemplate = scriptNameTemplate.format(osName) with open(os.path.join(os.path.dirname(__file__), scriptNameTemplate)) as f: script = f.read() # Reads signature with open(os.path.join(os.path.dirname(__file__), scriptNameTemplate + '.signature')) as f: signature = f.read() return script, signature, params
class SimpleLDAPAuthenticator(auths.Authenticator): host = gui.TextField(length=64, label=_('Host'), order=1, tooltip=_('Ldap Server IP or Hostname'), required=True) port = gui.NumericField( length=5, label=_('Port'), defvalue='389', order=2, tooltip=_('Ldap port (usually 389 for non ssl and 636 for ssl)'), required=True) ssl = gui.CheckBoxField( label=_('Use SSL'), order=3, tooltip= _('If checked, the connection will be ssl, using port 636 instead of 389' )) username = gui.TextField( length=64, label=_('Ldap User'), order=4, tooltip=_('Username with read privileges on the base selected'), required=True, tab=gui.CREDENTIALS_TAB) password = gui.PasswordField(lenth=32, label=_('Password'), order=5, tooltip=_('Password of the ldap user'), required=True, tab=gui.CREDENTIALS_TAB) timeout = gui.NumericField( length=3, label=_('Timeout'), defvalue='10', order=6, tooltip=_('Timeout in seconds of connection to LDAP'), required=True, minValue=1) ldapBase = gui.TextField( length=64, label=_('Base'), order=7, tooltip=_('Common search base (used for "users" and "groups")'), required=True, tab=_('Ldap info')) userClass = gui.TextField( length=64, label=_('User class'), defvalue='posixAccount', order=8, tooltip=_('Class for LDAP users (normally posixAccount)'), required=True, tab=_('Ldap info')) userIdAttr = gui.TextField( length=64, label=_('User Id Attr'), defvalue='uid', order=9, tooltip=_('Attribute that contains the user id'), required=True, tab=_('Ldap info')) userNameAttr = gui.TextField( length=64, label=_('User Name Attr'), defvalue='uid', order=10, tooltip= _('Attributes that contains the user name (list of comma separated values)' ), required=True, tab=_('Ldap info')) groupClass = gui.TextField( length=64, label=_('Group class'), defvalue='posixGroup', order=11, tooltip=_('Class for LDAP groups (normally poxisGroup)'), required=True, tab=_('Ldap info')) groupIdAttr = gui.TextField( length=64, label=_('Group Id Attr'), defvalue='cn', order=12, tooltip=_('Attribute that contains the group id'), required=True, tab=_('Ldap info')) memberAttr = gui.TextField( length=64, label=_('Group membership attr'), defvalue='memberUid', order=13, tooltip=_( 'Attribute of the group that contains the users belonging to it'), required=True, tab=_('Ldap info')) typeName = _('SimpleLDAP Authenticator') typeType = 'SimpleLdapAuthenticator' typeDescription = _('Simple LDAP authenticator') iconFile = 'auth.png' # If it has and external source where to get "new" users (groups must be declared inside UDS) isExternalSource = True # If we need to enter the password for this user needsPassword = False # Label for username field userNameLabel = _('Username') # Label for group field groupNameLabel = _("Group") # Label for password field passwordLabel = _("Password") _connection: typing.Any = None _host: str = '' _port: str = '' _ssl: bool = False _username: str = '' _password: str = '' _timeout: str = '' _ldapBase: str = '' _userClass: str = '' _groupClass: str = '' _userIdAttr: str = '' _groupIdAttr: str = '' _memberAttr: str = '' _userNameAttr: str = '' def initialize( self, values: typing.Optional[typing.Dict[str, typing.Any]]) -> None: if values: self._host = values['host'] self._port = values['port'] self._ssl = gui.strToBool(values['ssl']) self._username = values['username'] self._password = values['password'] self._timeout = values['timeout'] self._ldapBase = values['ldapBase'] self._userClass = values['userClass'] self._groupClass = values['groupClass'] self._userIdAttr = values['userIdAttr'] self._groupIdAttr = values['groupIdAttr'] self._memberAttr = values['memberAttr'] self._userNameAttr = values['userNameAttr'].replace( ' ', '') # Removes white spaces def valuesDict(self) -> gui.ValuesDictType: return { 'host': self._host, 'port': self._port, 'ssl': gui.boolToStr(self._ssl), 'username': self._username, 'password': self._password, 'timeout': self._timeout, 'ldapBase': self._ldapBase, 'userClass': self._userClass, 'groupClass': self._groupClass, 'userIdAttr': self._userIdAttr, 'groupIdAttr': self._groupIdAttr, 'memberAttr': self._memberAttr, 'userNameAttr': self._userNameAttr } def marshal(self) -> bytes: return '\t'.join([ 'v1', self._host, self._port, gui.boolToStr(self._ssl), self._username, self._password, self._timeout, self._ldapBase, self._userClass, self._groupClass, self._userIdAttr, self._groupIdAttr, self._memberAttr, self._userNameAttr ]).encode('utf8') def unmarshal(self, data: bytes): vals = data.decode('utf8').split('\t') if vals[0] == 'v1': logger.debug("Data: %s", vals[1:]) (self._host, self._port, ssl, self._username, self._password, self._timeout, self._ldapBase, self._userClass, self._groupClass, self._userIdAttr, self._groupIdAttr, self._memberAttr, self._userNameAttr) = vals[1:] self._ssl = gui.strToBool(ssl) def __connection(self, username: typing.Optional[str] = None, password: typing.Optional[str] = None): """ Tries to connect to ldap. If username is None, it tries to connect using user provided credentials. @return: Connection established @raise exception: If connection could not be established """ if self._connection is None: # We want this method also to check credentials self._connection = ldaputil.connection(self._username, self._password, self._host, port=int(self._port), ssl=self._ssl, timeout=int(self._timeout), debug=False) return self._connection def __connectAs(self, username: str, password: str) -> typing.Any: return ldaputil.connection(username, password, self._host, ssl=self._ssl, timeout=int(self._timeout), debug=False) def __getUser(self, username: str) -> typing.Optional[ldaputil.LDAPResultType]: """ Searchs for the username and returns its LDAP entry @param username: username to search, using user provided parameters at configuration to map search entries. @return: None if username is not found, an dictionary of LDAP entry attributes if found. @note: Active directory users contains the groups it belongs to in "memberOf" attribute """ return ldaputil.getFirst( con=self.__connection(), base=self._ldapBase, objectClass=self._userClass, field=self._userIdAttr, value=username, attributes=[ i for i in self._userNameAttr.split(',') + [self._userIdAttr] ], sizeLimit=LDAP_RESULT_LIMIT) def __getGroup(self, groupName: str) -> typing.Optional[ldaputil.LDAPResultType]: """ Searchs for the groupName and returns its LDAP entry @param groupName: group name to search, using user provided parameters at configuration to map search entries. @return: None if group name is not found, an dictionary of LDAP entry attributes if found. """ return ldaputil.getFirst(con=self.__connection(), base=self._ldapBase, objectClass=self._groupClass, field=self._groupIdAttr, value=groupName, attributes=[self._memberAttr], sizeLimit=LDAP_RESULT_LIMIT) def __getGroups(self, user: ldaputil.LDAPResultType): try: groups: typing.List[str] = [] filter_ = '(&(objectClass=%s)(|(%s=%s)(%s=%s)))' % ( self._groupClass, self._memberAttr, user['_id'], self._memberAttr, user['dn']) for d in ldaputil.getAsDict(con=self.__connection(), base=self._ldapBase, ldapFilter=filter_, attrList=[self._groupIdAttr], sizeLimit=10 * LDAP_RESULT_LIMIT): if self._groupIdAttr in d: for k in d[self._groupIdAttr]: groups.append(k) logger.debug('Groups: %s', groups) return groups except Exception: logger.exception('Exception at __getGroups') return [] def __getUserRealName(self, usr: ldaputil.LDAPResultType) -> str: ''' Tries to extract the real name for this user. Will return all atttributes (joint) specified in _userNameAttr (comma separated). ''' return ' '.join([ ' '.join((str(k) for k in usr.get(id_, ''))) if isinstance( usr.get(id_), list) else str(usr.get(id_, '')) for id_ in self._userNameAttr.split(',') ]).strip() def authenticate(self, username: str, credentials: str, groupsManager: 'auths.GroupsManager') -> bool: ''' Must authenticate the user. We can have to different situations here: 1.- The authenticator is external source, what means that users may be unknown to system before callig this 2.- The authenticator isn't external source, what means that users have been manually added to system and are known before this call We receive the username, the credentials used (normally password, but can be a public key or something related to pk) and a group manager. The group manager is responsible for letting know the authenticator which groups we currently has active. @see: uds.core.auths.groups_manager ''' try: # Locate the user at LDAP user = self.__getUser(username) if user is None: return False # Let's see first if it credentials are fine self.__connectAs( user['dn'], credentials) # Will raise an exception if it can't connect groupsManager.validate(self.__getGroups(user)) return True except Exception: return False def createUser(self, usrData: typing.Dict[str, str]) -> None: ''' Groups are only used in case of internal users (non external sources) that must know to witch groups this user belongs to @param usrData: Contains data received from user directly, that is, a dictionary with at least: name, realName, comments, state & password @return: Raises an exception (AuthException) it things didn't went fine ''' res = self.__getUser(usrData['name']) if res is None: raise auths.AuthenticatorException(_('Username not found')) # Fills back realName field usrData['real_name'] = self.__getUserRealName(res) def getRealName(self, username: str) -> str: ''' Tries to get the real name of an user ''' res = self.__getUser(username) if res is None: return username return self.__getUserRealName(res) def modifyUser(self, usrData: typing.Dict[str, str]) -> None: ''' We must override this method in authenticators not based on external sources (i.e. database users, text file users, etc..) Modify user has no reason on external sources, so it will never be used (probably) Groups are only used in case of internal users (non external sources) that must know to witch groups this user belongs to @param usrData: Contains data received from user directly, that is, a dictionary with at least: name, realName, comments, state & password @return: Raises an exception it things don't goes fine ''' return self.createUser(usrData) def createGroup(self, groupData: typing.Dict[str, str]) -> None: ''' We must override this method in authenticators not based on external sources (i.e. database users, text file users, etc..) External sources already has its own groups and, at most, it can check if it exists on external source before accepting it Groups are only used in case of internal users (non external sources) that must know to witch groups this user belongs to @params groupData: a dict that has, at least, name, comments and active @return: Raises an exception it things don't goes fine ''' res = self.__getGroup(groupData['name']) if res is None: raise auths.AuthenticatorException(_('Group not found')) def getGroups(self, username: str, groupsManager: 'auths.GroupsManager'): ''' Looks for the real groups to which the specified user belongs Updates groups manager with valid groups Remember to override it in derived authentication if needed (external auths will need this, for internal authenticators this is never used) ''' user = self.__getUser(username) if user is None: raise auths.AuthenticatorException(_('Username not found')) groupsManager.validate(self.__getGroups(user)) def searchUsers(self, pattern: str) -> typing.Iterable[typing.Dict[str, str]]: try: res = [] for r in ldaputil.getAsDict( con=self.__connection(), base=self._ldapBase, ldapFilter='(&(objectClass=%s)(%s=%s*))' % (self._userClass, self._userIdAttr, pattern), attrList=[self._userIdAttr, self._userNameAttr], sizeLimit=LDAP_RESULT_LIMIT): res.append({ 'id': r[self._userIdAttr][0], # Ignore @... 'name': self.__getUserRealName(r) }) return res except Exception: logger.exception("Exception: ") raise auths.AuthenticatorException( _('Too many results, be more specific')) def searchGroups(self, pattern: str) -> typing.Iterable[typing.Dict[str, str]]: try: res = [] for r in ldaputil.getAsDict( con=self.__connection(), base=self._ldapBase, ldapFilter='(&(objectClass=%s)(%s=%s*))' % (self._groupClass, self._groupIdAttr, pattern), attrList=[self._groupIdAttr, 'memberOf', 'description'], sizeLimit=LDAP_RESULT_LIMIT): res.append({ 'id': r[self._groupIdAttr][0], 'name': r['description'][0] }) return res except Exception: logger.exception("Exception: ") raise auths.AuthenticatorException( _('Too many results, be more specific')) @staticmethod def test(env, data): try: auth = SimpleLDAPAuthenticator(None, env, data) return auth.testConnection() except Exception as e: logger.error("Exception found testing Simple LDAP auth: %s", e) return [False, "Error testing connection"] def testConnection(self): # pylint: disable=too-many-return-statements,too-many-branches try: con = self.__connection() except Exception as e: return [False, str(e)] try: con.search_s(base=self._ldapBase, scope=ldap.SCOPE_BASE) except Exception: return [False, _('Ldap search base is incorrect')] try: if len( con.search_ext_s(base=self._ldapBase, scope=ldap.SCOPE_SUBTREE, filterstr='(objectClass=%s)' % self._userClass, sizelimit=1)) == 1: raise Exception() return [ False, _('Ldap user class seems to be incorrect (no user found by that class)' ) ] except Exception as e: # If found 1 or more, all right pass try: if len( con.search_ext_s(base=self._ldapBase, scope=ldap.SCOPE_SUBTREE, filterstr='(objectClass=%s)' % self._groupClass, sizelimit=1)) == 1: raise Exception() return [ False, _('Ldap group class seems to be incorrect (no group found by that class)' ) ] except Exception as e: # If found 1 or more, all right pass try: if len( con.search_ext_s(base=self._ldapBase, scope=ldap.SCOPE_SUBTREE, filterstr='(%s=*)' % self._userIdAttr, sizelimit=1)) == 1: raise Exception() return [ False, _('Ldap user id attribute seems to be incorrect (no user found by that attribute)' ) ] except Exception as e: # If found 1 or more, all right pass try: if len( con.search_ext_s(base=self._ldapBase, scope=ldap.SCOPE_SUBTREE, filterstr='(%s=*)' % self._groupIdAttr, sizelimit=1)) == 1: raise Exception() return [ False, _('Ldap group id attribute seems to be incorrect (no group found by that attribute)' ) ] except Exception as e: # If found 1 or more, all right pass # Now test objectclass and attribute of users try: if len( con.search_ext_s(base=self._ldapBase, scope=ldap.SCOPE_SUBTREE, filterstr='(&(objectClass=%s)(%s=*))' % (self._userClass, self._userIdAttr), sizelimit=1)) == 1: raise Exception() return [ False, _('Ldap user class or user id attr is probably wrong (can\'t find any user with both conditions)' ) ] except Exception as e: # If found 1 or more, all right pass # And group part, with membership try: res = con.search_ext_s(base=self._ldapBase, scope=ldap.SCOPE_SUBTREE, filterstr='(&(objectClass=%s)(%s=*))' % (self._groupClass, self._groupIdAttr), attrlist=[self._memberAttr.encode('utf-8')]) if not res: raise Exception( _('Ldap group class or group id attr is probably wrong (can\'t find any group with both conditions)' )) ok = False for r in res: if self._memberAttr in r[1]: ok = True break if ok is False: raise Exception( _('Can\'t locate any group with the membership attribute specified' )) except Exception as e: return [False, str(e)] return [ True, _("Connection params seem correct, test was succesfully executed") ] def __str__(self): return "Ldap Auth: {0}:{1}@{2}:{3}, base = {4}, userClass = {5}, groupClass = {6}, userIdAttr = {7}, groupIdAttr = {8}, memberAttr = {9}, userName attr = {10}".format( self._username, self._password, self._host, self._port, self._ldapBase, self._userClass, self._groupClass, self._userIdAttr, self._groupIdAttr, self._memberAttr, self._userNameAttr)
class TX2GOTransport(BaseX2GOTransport): """ Provides access via X2GO to service. This transport can use an domain. If username processed by authenticator contains '@', it will split it and left-@-part will be username, and right password """ typeName = _('X2Go') typeType = 'TX2GOTransport' typeDescription = _('X2Go access (Experimental). Tunneled connection.') group = transports.TUNNELED_GROUP tunnelServer = gui.TextField( label=_('Tunnel server'), order=1, tooltip= _('IP or Hostname of tunnel server sent to client device ("public" ip) and port. (use HOST:PORT format)' ), tab=gui.TUNNEL_TAB, ) tunnelWait = gui.NumericField( length=3, label=_('Tunnel wait time'), defvalue='30', minValue=5, maxValue=65536, order=2, tooltip=_('Maximum time to wait before closing the tunnel listener'), required=True, tab=gui.TUNNEL_TAB, ) verifyCertificate = gui.CheckBoxField( label=_('Force SSL certificate verification'), order=23, tooltip= _('If enabled, the certificate of tunnel server will be verified (recommended).' ), defvalue=gui.TRUE, tab=gui.TUNNEL_TAB, ) fixedName = BaseX2GOTransport.fixedName screenSize = BaseX2GOTransport.screenSize desktopType = BaseX2GOTransport.desktopType customCmd = BaseX2GOTransport.customCmd sound = BaseX2GOTransport.sound exports = BaseX2GOTransport.exports speed = BaseX2GOTransport.speed soundType = BaseX2GOTransport.soundType keyboardLayout = BaseX2GOTransport.keyboardLayout pack = BaseX2GOTransport.pack quality = BaseX2GOTransport.quality def initialize(self, values: 'Module.ValuesType'): if values: if values['tunnelServer'].count(':') != 1: raise BaseX2GOTransport.ValidationException( _('Must use HOST:PORT in Tunnel Server Field')) def getUDSTransportScript( # pylint: disable=too-many-locals self, userService: 'models.UserService', transport: 'models.Transport', ip: str, os: typing.Dict[str, str], user: '******', password: str, request: 'HttpRequest', ) -> typing.Tuple[str, str, typing.Mapping[str, typing.Any]]: ci = self.getConnectionInfo(userService, user, password) username = ci['username'] priv, pub = self.getAndPushKey(username, userService) width, height = self.getScreenSize() rootless = False desktop = self.desktopType.value if desktop == "UDSVAPP": desktop = "/usr/bin/udsvapp " + self.customCmd.value rootless = True xf = x2go_file.getTemplate( speed=self.speed.value, pack=self.pack.value, quality=self.quality.value, sound=self.sound.isTrue(), soundSystem=self.sound.value, windowManager=desktop, exports=self.exports.isTrue(), rootless=rootless, width=width, height=height, user=username, ) ticket = TicketStore.create_for_tunnel( userService=userService, port=22, validity=self.tunnelWait.num() + 60, # Ticket overtime ) tunHost, tunPort = self.tunnelServer.value.split(':') # data data = { 'os': os['OS'], 'ip': ip, 'port': 22, 'key': priv, 'width': width, 'height': height, 'printers': True, 'drives': self.exports.isTrue(), 'fullScreen': width == -1 or height == -1, 'this_server': request.build_absolute_uri('/'), 'xf': xf, } m = tools.DictAsObj(data) osName = { OsDetector.Windows: 'windows', OsDetector.Linux: 'linux', # OsDetector.Macintosh: 'macosx' }.get(os['OS']) if osName is None: return super().getUDSTransportScript(userService, transport, ip, os, user, password, request) sp = { 'tunHost': tunHost, 'tunPort': tunPort, 'tunWait': self.tunnelWait.num(), 'tunChk': self.verifyCertificate.isTrue(), 'ticket': ticket, 'key': priv, 'xf': xf, } return self.getScript('scripts/{}/tunnel.py', osName, sp)
class IPMachinesService(IPServiceBase): # Gui # Gui token = gui.TextField( order=1, label=_('Service Token'), length=16, tooltip=_('Service token that will be used by actors to communicate with service. Leave empty for persistent assignation.'), defvalue='', required=False, rdonly=False ) ipList = gui.EditableList(label=_('List of servers'), tooltip=_('List of servers available for this service')) port = gui.NumericField(length=5, label=_('Check Port'), defvalue='0', order=2, tooltip=_('If non zero, only Machines responding to connection on that port will be used'), required=True, tab=gui.ADVANCED_TAB) # Description of service typeName = _('Static Multiple IP') typeType = 'IPMachinesService' typeDescription = _('This service provides access to POWERED-ON Machines by IP') iconFile = 'machines.png' # Characteristics of service maxDeployed = -1 # If the service provides more than 1 "provided service" (-1 = no limit, 0 = ???? (do not use it!!!), N = max number to deploy usesCache = False # Cache are running machine awaiting to be assigned usesCache_L2 = False # L2 Cache are running machines in suspended state needsManager = False # If the service needs a s.o. manager (managers are related to agents provided by services itselfs, i.e. virtual machines with agent) mustAssignManually = False # If true, the system can't do an automatic assignation of a deployed user service from this service deployedType = IPMachineDeployed servicesTypeProvided = (serviceTypes.VDI,) _ips: typing.List[str] = [] _token: str = '' _port: int = 0 def initialize(self, values: 'Module.ValuesType') -> None: if values is None: return if values.get('ipList', None) is None: self._ips = [] else: self._ips = ['{}~{}'.format(str(ip).strip(), i) for i, ip in enumerate(values['ipList']) if str(ip).strip()] # Allow duplicates right now # self._ips.sort() self._token = self.token.value.strip() self._port = self.port.value def getToken(self): return self._token or None def valuesDict(self) -> gui.ValuesDictType: ips = (i.split('~')[0] for i in self._ips) return {'ipList': gui.convertToList(ips), 'token': self._token, 'port': str(self._port)} def marshal(self) -> bytes: self.storage.saveData('ips', pickle.dumps(self._ips)) return b'\0'.join([b'v3', self._token.encode(), str(self._port).encode()]) def unmarshal(self, data: bytes) -> None: values: typing.List[bytes] = data.split(b'\0') d = self.storage.readData('ips') if isinstance(d, bytes): self._ips = pickle.loads(d) elif isinstance(d, str): # "legacy" saved elements self._ips = pickle.loads(d.encode('utf8')) self.marshal() # Ensure now is bytes.. else: self._ips = [] if values[0] != b'v1': self._token = values[1].decode() if values[0] == b'v3': self._port = int(values[2].decode()) def getUnassignedMachine(self) -> typing.Optional[str]: # Search first unassigned machine try: for ip in self._ips: theIP = ip.split('~')[0] if self.storage.readData(theIP) is None: self.storage.saveData(theIP, theIP) # Now, check if it is available on port, if required... if self._port > 0: if connection.testServer(theIP, self._port, timeOut=0.5) is False: # Log into logs of provider, so it can be "shown" on services logs self.parent().doLog(log.WARN, 'Host {} not accesible on port {}'.format(theIP, self._port)) self.storage.remove(theIP) # Return Machine to pool continue return theIP return None except Exception: logger.exception("Exception at getUnassignedMachine") return None def unassignMachine(self, ip: str) -> None: try: self.storage.remove(ip) except Exception: logger.exception("Exception at getUnassignedMachine") def listAssignables(self): return [(ip, ip.split('~')[0]) for ip in self._ips if self.storage.readData(ip) is None] def assignFromAssignables(self, assignableId: str, user: '******', userDeployment: 'services.UserDeployment') -> str: userServiceInstance: IPMachineDeployed = typing.cast(IPMachineDeployed, userDeployment) theIP = assignableId.split('~')[0] if self.storage.readData(theIP) is None: self.storage.saveData(theIP, theIP) return userServiceInstance.assign(theIP) return userServiceInstance.error('IP already assigned')
class Provider(ServiceProvider): ''' This class represents the sample services provider In this class we provide: * The Provider functionality * The basic configuration parameters for the provider * The form fields needed by administrators to configure this provider :note: At class level, the translation must be simply marked as so using ugettext_noop. This is so cause we will translate the string when sent to the administration client. For this class to get visible at administration client as a provider type, we MUST register it at package __init__. ''' # : What kind of services we offer, this are classes inherited from Service offers = [LiveService] # : Name to show the administrator. This string will be translated BEFORE # : sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) typeName = _('OpenNebula Platform Provider') # : Type used internally to identify this provider typeType = 'openNebulaPlatform' # : Description shown at administration interface for this provider typeDescription = _('OpenNebula platform service provider') # : Icon file used as icon for this provider. This string will be translated # : BEFORE sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) iconFile = 'provider.png' # now comes the form fields # There is always two fields that are requested to the admin, that are: # Service Name, that is a name that the admin uses to name this provider # Description, that is a short description that the admin gives to this provider # Now we are going to add a few fields that we need to use this provider # Remember that these are "dummy" fields, that in fact are not required # but used for sample purposes # If we don't indicate an order, the output order of fields will be # "random" host = gui.TextField(length=64, label=_('Host'), order=1, tooltip=_('OpenNebula Host'), required=True) port = gui.NumericField( length=5, label=_('Port'), defvalue='2633', order=2, tooltip=_('OpenNebula Port (default is 2633 for non ssl connection)'), required=True) ssl = gui.CheckBoxField( label=_('Use SSL'), order=3, tooltip= _('If checked, the connection will be forced to be ssl (will not work if server is not providing ssl)' )) username = gui.TextField( length=32, label=_('Username'), order=4, tooltip=_('User with valid privileges on OpenNebula'), required=True, defvalue='oneadmin') password = gui.PasswordField( lenth=32, label=_('Password'), order=5, tooltip=_('Password of the user of OpenNebula'), required=True) maxPreparingServices = gui.NumericField( length=3, label=_('Creation concurrency'), defvalue='10', minValue=1, maxValue=65536, order=50, tooltip=_('Maximum number of concurrently creating VMs'), required=True, tab=gui.ADVANCED_TAB) maxRemovingServices = gui.NumericField( length=3, label=_('Removal concurrency'), defvalue='5', minValue=1, maxValue=65536, order=51, tooltip=_('Maximum number of concurrently removing VMs'), required=True, tab=gui.ADVANCED_TAB) timeout = gui.NumericField( length=3, label=_('Timeout'), defvalue='10', order=90, tooltip=_('Timeout in seconds of connection to OpenNebula'), required=True, tab=gui.ADVANCED_TAB) # Own variables _api = None def initialize(self, values=None): ''' We will use the "autosave" feature for form fields ''' # Just reset _api connection variable self._api = None if values is not None: self.timeout.value = validators.validateTimeout( self.timeout.value, returnAsInteger=False) logger.debug('Endpoint: {}'.format(self.endpoint)) @property def endpoint(self): return 'http{}://{}:{}/RPC2'.format('s' if self.ssl.isTrue() else '', self.host.value, self.port.value) @property def api(self): if self._api is None: self._api = on.OpenNebulaClient(self.username.value, self.password.value, self.endpoint) logger.debug('Api: {}'.format(self._api)) return self._api def resetApi(self): self._api = None def sanitizeVmName(self, name): return on.sanitizeName(name) def testConnection(self): ''' Test that conection to OpenNebula server is fine Returns True if all went fine, false if id didn't ''' try: if self.api.version[0] < '4': return [ False, 'OpenNebula version is not supported (required version 4.1 or newer)' ] except Exception as e: return [False, '{}'.format(e)] return [True, _('Opennebula test connection passed')] def getDatastores(self, datastoreType=0): return on.storage.enumerateDatastores(self.api, datastoreType) def getTemplates(self, force=False): return on.template.getTemplates(self.api, force) def makeTemplate(self, fromTemplateId, name, toDataStore): return on.template.create(self.api, fromTemplateId, name, toDataStore) def checkTemplatePublished(self, templateId): return on.template.checkPublished(self.api, templateId) def removeTemplate(self, templateId): return on.template.remove(self.api, templateId) def deployFromTemplate(self, name, templateId): return on.template.deployFrom(self.api, templateId, name) def getMachineState(self, machineId): ''' Returns the state of the machine This method do not uses cache at all (it always tries to get machine state from OpenNebula server) Args: machineId: Id of the machine to get state Returns: one of the on.VmState Values ''' return on.vm.getMachineState(self.api, machineId) def getMachineSubState(self, machineId): ''' Returns the LCM_STATE of a machine (must be ready) ''' return on.vm.getMachineSubstate(self.api, machineId) def startMachine(self, machineId): ''' Tries to start a machine. No check is done, it is simply requested to OpenNebula. This start also "resume" suspended/paused machines Args: machineId: Id of the machine Returns: ''' on.vm.startMachine(self.api, machineId) def stopMachine(self, machineId): ''' Tries to start a machine. No check is done, it is simply requested to OpenNebula Args: machineId: Id of the machine Returns: ''' on.vm.stopMachine(self.api, machineId) def suspendMachine(self, machineId): ''' Tries to start a machine. No check is done, it is simply requested to OpenNebula Args: machineId: Id of the machine Returns: ''' on.vm.suspendMachine(self.api, machineId) def removeMachine(self, machineId): ''' Tries to delete a machine. No check is done, it is simply requested to OpenNebula Args: machineId: Id of the machine Returns: ''' on.vm.removeMachine(self.api, machineId) def getNetInfo(self, machineId, networkId=None): ''' Changes the mac address of first nic of the machine to the one specified ''' return on.vm.getNetInfo(self.api, machineId, networkId) def getConsoleConnection(self, machineId): display = on.vm.getDisplayConnection(self.api, machineId) return { 'type': display['type'], 'address': display['host'], 'port': display['port'], 'secure_port': -1, 'monitors': 1, 'cert_subject': '', 'ticket': { 'value': display['passwd'], 'expiry': '' } } @staticmethod def test(env, data): ''' Test ovirt Connectivity Args: env: environment passed for testing (temporal environment passed) data: data passed for testing (data obtained from the form definition) Returns: Array of two elements, first is True of False, depending on test (True is all right, false is error), second is an String with error, preferably internacionalizated.. ''' return Provider(env, data).testConnection()
class BaseX2GOTransport(transports.Transport): """ Provides access via X2GO to service. This transport can use an domain. If username processed by authenticator contains '@', it will split it and left-@-part will be username, and right password """ iconFile = 'x2go.png' protocol = transports.protocols.X2GO supportedOss = (OsDetector.Linux, OsDetector.Windows) fixedName = gui.TextField( order=2, label=_('Username'), tooltip=_( 'If not empty, this username will be always used as credential'), tab=gui.CREDENTIALS_TAB) screenSize = gui.ChoiceField(label=_('Screen size'), order=10, tooltip=_('Screen size'), defvalue=CommonPrefs.SZ_FULLSCREEN, values=[{ 'id': CommonPrefs.SZ_640x480, 'text': '640x480' }, { 'id': CommonPrefs.SZ_800x600, 'text': '800x600' }, { 'id': CommonPrefs.SZ_1024x768, 'text': '1024x768' }, { 'id': CommonPrefs.SZ_1366x768, 'text': '1366x768' }, { 'id': CommonPrefs.SZ_1920x1080, 'text': '1920x1080' }, { 'id': CommonPrefs.SZ_FULLSCREEN, 'text': ugettext_lazy('Full Screen') }], tab=gui.PARAMETERS_TAB) desktopType = gui.ChoiceField( label=_('Desktop'), order=11, tooltip=_('Desktop session'), values=[ { 'id': 'XFCE', 'text': 'Xfce' }, { 'id': 'MATE', 'text': 'Mate' }, { 'id': 'LXDE', 'text': 'Lxde' }, { 'id': 'GNOME', 'text': 'Gnome (see docs)' }, { 'id': 'KDE', 'text': 'Kde (see docs)' }, # {'id': 'UNITY', 'text': 'Unity (see docs)'}, { 'id': 'gnome-session-cinnamon', 'text': 'Cinnamon 1.4 (see docs)' }, { 'id': 'gnome-session-cinnamon2d', 'text': 'Cinnamon 2.2 (see docs)' }, { 'id': 'UDSVAPP', 'text': 'UDS vAPP' }, ], tab=gui.PARAMETERS_TAB) customCmd = gui.TextField( order=12, label=_('vAPP'), tooltip= _('If UDS vAPP is selected as "Desktop", the FULL PATH of the app to be executed. If UDS vAPP is not selected, this field will be ignored.' ), tab=gui.PARAMETERS_TAB) sound = gui.CheckBoxField(order=13, label=_('Enable sound'), tooltip=_('If checked, sound will be available'), defvalue=gui.TRUE, tab=gui.PARAMETERS_TAB) exports = gui.CheckBoxField( order=14, label=_('Redirect home folder'), tooltip= _('If checked, user home folder will be redirected. (On linux, also redirects /media)' ), defvalue=gui.FALSE, tab=gui.PARAMETERS_TAB) speed = gui.ChoiceField(label=_('Speed'), order=15, tooltip=_('Connection speed'), defvalue='3', values=[ { 'id': '0', 'text': 'MODEM' }, { 'id': '1', 'text': 'ISDN' }, { 'id': '2', 'text': 'ADSL' }, { 'id': '3', 'text': 'WAN' }, { 'id': '4', 'text': 'LAN' }, ], tab=gui.PARAMETERS_TAB) soundType = gui.ChoiceField(label=_('Sound'), order=30, tooltip=_('Sound server'), defvalue='pulse', values=[ { 'id': 'pulse', 'text': 'Pulse' }, { 'id': 'esd', 'text': 'ESD' }, ], tab=gui.ADVANCED_TAB) keyboardLayout = gui.TextField( label=_('Keyboard'), order=31, tooltip=_( 'Keyboard layout (es, us, fr, ...). Empty value means autodetect.' ), defvalue='', tab=gui.ADVANCED_TAB) # 'nopack', '8', '64', '256', '512', '4k', '32k', '64k', '256k', '2m', '16m' # '256-rdp', '256-rdp-compressed', '32k-rdp', '32k-rdp-compressed', '64k-rdp' # '64k-rdp-compressed', '16m-rdp', '16m-rdp-compressed' # 'rfb-hextile', 'rfb-tight', 'rfb-tight-compressed' # '8-tight', '64-tight', '256-tight', '512-tight', '4k-tight', '32k-tight' # '64k-tight', '256k-tight', '2m-tight', '16m-tight' # '8-jpeg-%', '64-jpeg', '256-jpeg', '512-jpeg', '4k-jpeg', '32k-jpeg' # '64k-jpeg', '256k-jpeg', '2m-jpeg', '16m-jpeg-%' # '8-png-jpeg-%', '64-png-jpeg', '256-png-jpeg', '512-png-jpeg', '4k-png-jpeg' # '32k-png-jpeg', '64k-png-jpeg', '256k-png-jpeg', '2m-png-jpeg', '16m-png-jpeg-%' # '8-png-%', '64-png', '256-png', '512-png', '4k-png' # '32k-png', '64k-png', '256k-png', '2m-png', '16m-png-%' # '16m-rgb-%', '16m-rle-%' pack = gui.TextField(label=_('Pack'), order=32, tooltip=_('Pack format. Change with care!'), defvalue='16m-jpeg', tab=gui.ADVANCED_TAB) quality = gui.NumericField( label=_('Quality'), order=33, tooltip=_('Quality value used on some pack formats.'), length=1, defvalue='6', minValue=1, maxValue=9, required=True, tab=gui.ADVANCED_TAB) def isAvailableFor(self, userService: 'models.UserService', ip: str) -> bool: """ Checks if the transport is available for the requested destination ip Override this in yours transports """ logger.debug('Checking availability for %s', ip) ready = self.cache.get(ip) if ready is None: # Check again for ready if connection.testServer(ip, 22): self.cache.put(ip, 'Y', READY_CACHE_TIMEOUT) return True self.cache.put(ip, 'N', READY_CACHE_TIMEOUT) return ready == 'Y' def getScreenSize(self) -> typing.Tuple[int, int]: return CommonPrefs.getWidthHeight(self.screenSize.value) def processedUser(self, userService: 'models.UserService', user: '******') -> str: v = self.processUserPassword(userService, user, '') return v['username'] def processUserPassword(self, userService: 'models.UserService', user: '******', password: str) -> typing.Dict[str, str]: username = user.getUsernameForAuth() if self.fixedName.value != '': username = self.fixedName.value # Fix username/password acording to os manager username, password = userService.processUserPassword( username, password) return { 'protocol': self.protocol, 'username': username, 'password': '' } def getConnectionInfo(self, userService: typing.Union['models.UserService', 'models.ServicePool'], user: '******', password: str) -> typing.Dict[str, str]: return self.processUserPassword(userService, user, password) def genKeyPairForSsh(self) -> typing.Tuple[str, str]: """ Generates a key pair for use with x2go The private part is used by client the public part must be "appended" to authorized_keys if it is not already added. If .ssh folder does not exists, it must be created if authorized_keys does not exists, it must be created On key adition, we can look for every key that has a "UDS@X2GOCLIENT" as comment, so we can remove them before adding new ones Windows (tested): C:\\Program Files (x86)\\x2goclient>x2goclient.exe --session-conf=c:/temp/sessions --session=UDS/test-session --close-disconnect --hide --no-menu Linux (tested): HOME=[temporal folder, where we create a .x2goclient folder and a sessions inside] pyhoca-cli -P UDS/test-session """ key = paramiko.RSAKey.generate(SSH_KEY_LENGTH) privFile = io.StringIO() key.write_private_key(privFile) priv = privFile.getvalue() pub = key.get_base64( ) # 'ssh-rsa {} UDS@X2GOCLIENT'.format(key.get_base64()) return priv, pub def getAuthorizeScript(self, user: str, pubKey: str) -> str: with open( os.path.join(os.path.dirname(__file__), 'scripts/authorize.py')) as f: data = f.read() return data.replace('__USER__', user).replace('__KEY__', pubKey) def getAndPushKey( self, userName: str, userService: 'models.UserService') -> typing.Tuple[str, str]: priv, pub = self.genKeyPairForSsh() authScript = self.getAuthorizeScript(userName, pub) userServiceManager().sendScript(userService, authScript) return priv, pub def getScript( self, scriptNameTemplate: str, osName: str, params: typing.Dict[str, typing.Any] ) -> typing.Tuple[str, str, typing.Dict[str, typing.Any]]: # Reads script scriptNameTemplate = scriptNameTemplate.format(osName) with open(os.path.join(os.path.dirname(__file__), scriptNameTemplate)) as f: script = f.read() # Reads signature with open( os.path.join(os.path.dirname(__file__), scriptNameTemplate + '.signature')) as f: signature = f.read() return script, signature, params
class ProxmoxProvider(services.ServiceProvider): # pylint: disable=too-many-public-methods offers = [ProxmoxLinkedService] typeName = _('Proxmox Platform Provider') typeType = 'ProxmoxPlatform' typeDescription = _('Proxmox platform service provider') iconFile = 'provider.png' host = gui.TextField(length=64, label=_('Host'), order=1, tooltip=_('Proxmox Server IP or Hostname'), required=True) port = gui.NumericField(lengh=5, label=_('Port'), order=2, tooltip=_('Proxmox API port (default is 8006)'), required=True, defvalue='8006') username = gui.TextField( length=32, label=_('Username'), order=3, tooltip= _('User with valid privileges on Proxmox, (use "user@authenticator" form)' ), required=True, defvalue='root@pam') password = gui.PasswordField(lenth=32, label=_('Password'), order=4, tooltip=_('Password of the user of Proxmox'), required=True) maxPreparingServices = gui.NumericField( length=3, label=_('Creation concurrency'), defvalue='10', minValue=1, maxValue=65536, order=50, tooltip=_('Maximum number of concurrently creating VMs'), required=True, tab=gui.ADVANCED_TAB) maxRemovingServices = gui.NumericField( length=3, label=_('Removal concurrency'), defvalue='5', minValue=1, maxValue=65536, order=51, tooltip=_('Maximum number of concurrently removing VMs'), required=True, tab=gui.ADVANCED_TAB) timeout = gui.NumericField( length=3, label=_('Timeout'), defvalue='20', order=90, tooltip=_('Timeout in seconds of connection to Proxmox'), required=True, tab=gui.ADVANCED_TAB) # Own variables _api: typing.Optional[client.ProxmoxClient] = None def __getApi(self) -> client.ProxmoxClient: """ Returns the connection API object """ if self._api is None: self._api = client.ProxmoxClient(self.host.value, self.port.num(), self.username.value, self.password.value, self.timeout.num(), False, self.cache) return self._api # There is more fields type, but not here the best place to cover it def initialize(self, values: 'Module.ValuesType') -> None: """ We will use the "autosave" feature for form fields """ # Just reset _api connection variable self._api = None if values is not None: self.timeout.value = validators.validateTimeout(self.timeout.value) logger.debug(self.host.value) def testConnection(self) -> bool: """ Test that conection to Proxmox server is fine Returns True if all went fine, false if id didn't """ return self.__getApi().test() def listMachines(self) -> typing.List[client.types.VMInfo]: return self.__getApi().listVms() def getMachineInfo(self, vmId: int) -> client.types.VMInfo: return self.__getApi().getVmInfo(vmId, force=True) def getMachineConfiguration(self, vmId: int) -> client.types.VMConfiguration: return self.__getApi().getVmConfiguration(vmId, force=True) def getStorageInfo(self, storageId: str, node: str) -> client.types.StorageInfo: return self.__getApi().getStorage(storageId, node) def listStorages( self, node: typing.Optional[str] ) -> typing.List[client.types.StorageInfo]: return self.__getApi().listStorages(node=node, content='images') def listPools(self) -> typing.List[client.types.PoolInfo]: return self.__getApi().listPools() def makeTemplate(self, vmId: int) -> None: return self.__getApi().convertToTemplate(vmId) def cloneMachine(self, vmId: int, name: str, description: typing.Optional[str], linkedClone: bool, toNode: typing.Optional[str] = None, toStorage: typing.Optional[str] = None, toPool: typing.Optional[str] = None, memory: int = 0) -> client.types.VmCreationResult: return self.__getApi().cloneVm(vmId, name, description, linkedClone, toNode, toStorage, toPool, memory) def startMachine(self, vmId: int) -> client.types.UPID: return self.__getApi().startVm(vmId) def stopMachine(self, vmId: int) -> client.types.UPID: return self.__getApi().stopVm(vmId) def suspendMachine(self, vmId: int) -> client.types.UPID: return self.__getApi().suspendVm(vmId) def removeMachine(self, vmId: int) -> client.types.UPID: return self.__getApi().deleteVm(vmId) def getTaskInfo(self, node: str, upid: str) -> client.types.TaskStatus: return self.__getApi().getTask(node, upid) def enableHA(self, vmId: int, started: bool = False, group: typing.Optional[str] = None) -> None: self.__getApi().enableVmHA(vmId, started, group) def disableHA(self, vmId: int) -> None: self.__getApi().disableVmHA(vmId) def listHaGroups(self) -> typing.List[str]: return self.__getApi().listHAGroups() def getConsoleConnection( self, machineId: str ) -> typing.Optional[typing.MutableMapping[str, typing.Any]]: # TODO: maybe proxmox also supports "spice"? for future release... return None @staticmethod def test(env: 'Environment', data: 'Module.ValuesType') -> typing.List[typing.Any]: """ Test ovirt Connectivity Args: env: environment passed for testing (temporal environment passed) data: data passed for testing (data obtained from the form definition) Returns: Array of two elements, first is True of False, depending on test (True is all right, false is error), second is an String with error, preferably internacionalizated.. """ # try: # # We instantiate the provider, but this may fail... # instance = Provider(env, data) # logger.debug('Methuselah has {0} years and is {1} :-)' # .format(instance.methAge.value, instance.methAlive.value)) # except ServiceProvider.ValidationException as e: # # If we say that meth is alive, instantiation will # return [False, str(e)] # except Exception as e: # logger.exception("Exception caugth!!!") # return [False, str(e)] # return [True, _('Nothing tested, but all went fine..')] prox = ProxmoxProvider(env, data) if prox.testConnection() is True: return [True, 'Test successfully passed'] return [False, _("Connection failed. Check connection params")]
class ServiceOne(Service): ''' Basic service, the first part (variables) include the description of the service. Remember to fill all variables needed, but at least you must define: * typeName * typeType * typeDescription * iconFile (defaults to service.png) * publicationType, type of publication in case it needs publication. If this is not provided, core will assume that the service do not needs publishing. * deployedType, type of deployed user service. Do not forget this!!! The rest of them can be ommited, but its recommended that you fill all declarations shown in this sample (that in fact, are all) This description informs the core what this service really provides, and how this is done. Look at description of class variables for more information. ''' #: Name to show the administrator. This string will be translated BEFORE #: sending it to administration interface, so don't forget to #: mark it as _ (using ugettext_noop) typeName = _('Sample Service One') #: Type used internally to identify this provider typeType = 'SampleService1' #: Description shown at administration interface for this provider typeDescription = _('Sample (and dummy) service ONE') #: Icon file used as icon for this provider. This string will be translated #: BEFORE sending it to administration interface, so don't forget to #: mark it as _ (using ugettext_noop) iconFile = 'service.png' # Functional related data #: If the service provides more than 1 "deployed user" (-1 = no limit, #: 0 = ???? (do not use it!!!), N = max number to deploy maxDeployed = -1 #: If we need to generate "cache" for this service, so users can access the #: provided services faster. Is usesCache is True, you will need also #: set publicationType, do take care about that! usesCache = False #: Tooltip shown to user when this item is pointed at admin interface, none #: because we don't use it cacheTooltip = _('None') #: If we need to generate a "Level 2" cache for this service (i.e., L1 #: could be running machines and L2 suspended machines) usesCache_L2 = False #: Tooltip shown to user when this item is pointed at admin interface, None #: also because we don't use it cacheTooltip_L2 = _('None') #: If the service needs a s.o. manager (managers are related to agents #: provided by services itselfs, i.e. virtual machines with actors) needsManager = False #: If true, the system can't do an automatic assignation of a deployed user #: service from this service mustAssignManually = False #: Types of publications (preparated data for deploys) #: In our case, we do no need a publication, so this is None publicationType = None #: Types of deploys (services in cache and/or assigned to users) deployedType = SampleUserDeploymentOne # Now the form part, this service will have only two "dummy" fields # If we don't indicate an order, the output order of fields will be # "random" colour = gui.ChoiceField( order=1, label=_('Colour'), tooltip=_('Colour of the field'), # In this case, the choice can have none value selected by default required=True, values=[ gui.choiceItem('red', 'Red'), gui.choiceItem('green', 'Green'), gui.choiceItem('blue', 'Blue'), gui.choiceItem('nonsense', 'Blagenta') ], defvalue='1' # Default value is the ID of the choicefield ) passw = gui.PasswordField( order=2, label=_('Password'), tooltip=_('Password for testing purposes'), required=True, defvalue='1234' #: Default password are nonsense?? :-) ) baseName = gui.TextField( order=3, label=_('Services names'), tooltip=_('Base name for this user services'), # In this case, the choice can have none value selected by default required=True, defvalue='' # Default value is the ID of the choicefield ) def initialize(self, values): ''' We check here form values to see if they are valid. Note that we check them throught FROM variables, that already has been initialized by __init__ method of base class, before invoking this. ''' # We don't need to check anything, bat because this is a sample, we do # As in provider, we receive values only at new Service creation, # so we only need to validate params if values is not None if values is not None: if self.colour.value == 'nonsense': raise Service.ValidationException( 'The selected colour is invalid!!!') # Services itself are non testeable right now, so we don't even have # to provide one!!! # Congratulations!!!, the needed part of your first simple service is done! # Now you can go to administration panel, and check it # # From now onwards, we implement our own methods, that will be used by, # for example, services derived from this provider def getColour(self): ''' Simply returns colour, for deployed user services. Remember that choiceField.value returns the id part of the ChoiceItem ''' return self.colour.value def getPassw(self): ''' Simply returns passwd, for deloyed user services ''' return self.passw.value def getBaseName(self): ''' ''' return self.baseName.value
class XenLinkedService(Service): """ Xen Linked clones service. This is based on creating a template from selected vm, and then use it to """ # : Name to show the administrator. This string will be translated BEFORE # : sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) typeName = _('Xen Linked Clone') # : Type used internally to identify this provider typeType = 'XenLinkedService' # : Description shown at administration interface for this provider typeDescription = _('Xen Services based on templates') # : Icon file used as icon for this provider. This string will be translated # : BEFORE sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) iconFile = 'service.png' # Functional related data # : If the service provides more than 1 "deployed user" (-1 = no limit, # : 0 = ???? (do not use it!!!), N = max number to deploy maxDeployed = -1 # : If we need to generate "cache" for this service, so users can access the # : provided services faster. Is usesCache is True, you will need also # : set publicationType, do take care about that! usesCache = True # : Tooltip shown to user when this item is pointed at admin interface, none # : because we don't use it cacheTooltip = _( 'Number of desired machines to keep running waiting for a user') # : If we need to generate a "Level 2" cache for this service (i.e., L1 # : could be running machines and L2 suspended machines) usesCache_L2 = True # : Tooltip shown to user when this item is pointed at admin interface, None # : also because we don't use it cacheTooltip_L2 = _( 'Number of desired machines to keep suspended waiting for use') # : If the service needs a s.o. manager (managers are related to agents # : provided by services itselfs, i.e. virtual machines with actors) needsManager = True # : If true, the system can't do an automatic assignation of a deployed user # : service from this service mustAssignManually = False canReset = True # : Types of publications (preparated data for deploys) # : In our case, we do no need a publication, so this is None publicationType = XenPublication # : Types of deploys (services in cache and/or assigned to users) deployedType = XenLinkedDeployment servicesTypeProvided = (serviceTypes.VDI, ) # Now the form part datastore = gui.ChoiceField( label=_("Storage SR"), rdonly=False, order=100, tooltip=_('Storage where to publish and put incrementals'), required=True) minSpaceGB = gui.NumericField(length=3, label=_('Reserved Space'), defvalue='32', order=101, tooltip=_('Minimal free space in GB'), required=True) machine = gui.ChoiceField(label=_("Base Machine"), order=110, tooltip=_('Service base machine'), tab=_('Machine'), required=True) network = gui.ChoiceField(label=_("Network"), rdonly=False, order=111, tooltip=_('Network used for virtual machines'), tab=_('Machine'), required=True) memory = gui.NumericField(label=_("Memory (Mb)"), length=4, defvalue=512, rdonly=False, order=112, tooltip=_('Memory assigned to machines'), tab=_('Machine'), required=True) shadow = gui.NumericField( label=_("Shadow"), length=1, defvalue=4, rdonly=False, order=113, tooltip=_('Shadow memory multiplier (memory overcommit)'), tab=_('Machine'), required=True) baseName = gui.TextField( label=_('Machine Names'), rdonly=False, order=114, tooltip=_('Base name for clones from this machine'), tab=_('Machine'), required=True) lenName = gui.NumericField( length=1, label=_('Name Length'), defvalue=5, order=115, tooltip= _('Length of numeric part for the names of this machines (beetwen 3 and 6' ), tab=_('Machine'), required=True) def initialize(self, values): """ We check here form values to see if they are valid. Note that we check them throught FROM variables, that already has been initialized by __init__ method of base class, before invoking this. """ if values is not None: length = int(self.lenName.value) if len(self.baseName.value) + length > 15: raise Service.ValidationException( _('The length of basename plus length must not be greater than 15' )) if self.baseName.value.isdigit(): raise Service.ValidationException( _('The machine name can\'t be only numbers')) if int(self.memory.value) < 256: raise Service.ValidationException( _('The minimum allowed memory is 256 Mb')) def initGui(self): """ Loads required values inside """ # Here we have to use "default values", cause values aren't used at form initialization # This is that value is always '', so if we want to change something, we have to do it # at defValue machines = self.parent().getMachines() storages = self.parent().getStorages() networks = self.parent().getNetworks() machines_list = [] for m in machines: machines_list.append(gui.choiceItem(m['id'], m['name'])) storages_list = [] for storage in storages: space, free = storage['size'] / 1024, (storage['size'] - storage['used']) / 1024 storages_list.append( gui.choiceItem( storage['id'], "%s (%4.2f Gb/%4.2f Gb)" % (storage['name'], space, free))) network_list = [] for net in networks: network_list.append(gui.choiceItem(net['id'], net['name'])) self.machine.setValues(machines_list) self.datastore.setValues(storages_list) self.network.setValues(network_list) def checkTaskFinished(self, task): return self.parent().checkTaskFinished(task) def datastoreHasSpace(self): # Get storages for that datacenter logger.debug('Checking datastore space for {0}'.format( self.datastore.value)) info = self.parent().getStorageInfo(self.datastore.value) logger.debug('Datastore Info: {0}'.format(info)) availableGB = (info['size'] - info['used']) / 1024 if availableGB < self.minSpaceGB.num(): raise Exception( 'Not enough free space available: (Needs at least {0} GB and there is only {1} GB ' .format(self.minSpaceGB.num(), availableGB)) def sanitizeVmName(self, name): """ Xen Seems to allow all kind of names """ return name def startDeployTemplate(self, name, comments): """ Invokes makeTemplate from parent provider, completing params Args: name: Name to assign to template (must be previously "sanitized" comments: Comments (UTF-8) to add to template Returns: template Id of the template created Raises an exception if operation fails. """ logger.debug( 'Starting deploy of template from machine {0} on datastore {1}'. format(self.machine.value, self.datastore.value)) # Checks datastore size self.datastoreHasSpace() return self.parent().cloneForTemplate(name, comments, self.machine.value, self.datastore.value) def convertToTemplate(self, machineId): """ """ self.parent().convertToTemplate(machineId, self.shadow.value) def startDeployFromTemplate(self, name, comments, templateId): """ Deploys a virtual machine on selected cluster from selected template Args: name: Name (sanitized) of the machine comments: Comments for machine templateId: Id of the template to deploy from displayType: 'vnc' or 'spice'. Display to use ad Xen admin interface memoryMB: Memory requested for machine, in MB guaranteedMB: Minimum memory guaranteed for this machine Returns: Id of the machine being created form template """ logger.debug('Deploying from template {0} machine {1}'.format( templateId, name)) self.datastoreHasSpace() return self.parent().startDeployFromTemplate(name, comments, templateId) def removeTemplate(self, templateId): """ invokes removeTemplate from parent provider """ return self.parent().removeTemplate(templateId) def getVMPowerState(self, machineId): """ Invokes getMachineState from parent provider Args: machineId: If of the machine to get state Returns: one of this values: """ return self.parent().getVMPowerState(machineId) def startVM(self, machineId, async=True): """ Tries to start a machine. No check is done, it is simply requested to Xen. This start also "resume" suspended/paused machines Args: machineId: Id of the machine Returns: """ return self.parent().startVM(machineId, async)
class OVirtLinkedService(Service): ''' oVirt Linked clones service. This is based on creating a template from selected vm, and then use it to ''' # : Name to show the administrator. This string will be translated BEFORE # : sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) typeName = _('oVirt/RHEV Linked Clone') # : Type used internally to identify this provider typeType = 'oVirtLinkedService' # : Description shown at administration interface for this provider typeDescription = _( 'oVirt Services based on templates and COW (experimental)') # : Icon file used as icon for this provider. This string will be translated # : BEFORE sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) iconFile = 'service.png' # Functional related data # : If the service provides more than 1 "deployed user" (-1 = no limit, # : 0 = ???? (do not use it!!!), N = max number to deploy maxDeployed = -1 # : If we need to generate "cache" for this service, so users can access the # : provided services faster. Is usesCache is True, you will need also # : set publicationType, do take care about that! usesCache = True # : Tooltip shown to user when this item is pointed at admin interface, none # : because we don't use it cacheTooltip = _( 'Number of desired machines to keep running waiting for a user') # : If we need to generate a "Level 2" cache for this service (i.e., L1 # : could be running machines and L2 suspended machines) usesCache_L2 = True # : Tooltip shown to user when this item is pointed at admin interface, None # : also because we don't use it cacheTooltip_L2 = _( 'Number of desired machines to keep suspended waiting for use') # : If the service needs a s.o. manager (managers are related to agents # : provided by services itselfs, i.e. virtual machines with actors) needsManager = True # : If true, the system can't do an automatic assignation of a deployed user # : service from this service mustAssignManually = False # : Types of publications (preparated data for deploys) # : In our case, we do no need a publication, so this is None publicationType = OVirtPublication # : Types of deploys (services in cache and/or assigned to users) deployedType = OVirtLinkedDeployment # Now the form part machine = gui.ChoiceField(label=_("Base Machine"), order=1, tooltip=_('Service base machine'), required=True) cluster = gui.ChoiceField(label=_("Cluster"), order=2, fills={ 'callbackName': 'ovFillResourcesFromCluster', 'function': oVirtHelpers.getResources, 'parameters': ['cluster', 'ov', 'ev'] }, tooltip=_("Cluster to contain services"), required=True) datastore = gui.ChoiceField( label=_("Datastore Domain"), rdonly=False, order=3, tooltip=_('Datastore domain where to publish and put incrementals'), required=True) minSpaceGB = gui.NumericField(length=3, label=_('Reserved Space'), defvalue='32', order=4, tooltip=_('Minimal free space in GB'), required=True) memory = gui.NumericField(label=_("Memory (Mb)"), length=4, defvalue=512, rdonly=False, order=5, tooltip=_('Memory assigned to machines'), required=True) memoryGuaranteed = gui.NumericField( label=_("Memory Guaranteed (Mb)"), length=4, defvalue=256, rdonly=False, order=6, tooltip=_('Physical memory guaranteed to machines'), required=True) baseName = gui.TextField( label=_('Machine Names'), rdonly=False, order=6, tooltip=('Base name for clones from this machine'), required=True) lenName = gui.NumericField( length=1, label=_('Name Length'), defvalue=5, order=7, tooltip= _('Size of numeric part for the names of these machines (between 3 and 6)' ), required=True) display = gui.ChoiceField( label=_('Display'), rdonly=False, order=8, tooltip=_('Display type (only for administration purposes)'), values=[ gui.choiceItem('spice', 'Spice'), gui.choiceItem('vnc', 'Vnc') ], defvalue='1' # Default value is the ID of the choicefield ) ov = gui.HiddenField() ev = gui.HiddenField( ) # We need to keep the env so we can instantiate the Provider def initialize(self, values): ''' We check here form values to see if they are valid. Note that we check them throught FROM variables, that already has been initialized by __init__ method of base class, before invoking this. ''' if values is not None: length = int(self.lenName.value) if len(self.baseName.value) + length > 15: raise Service.ValidationException( _('The length of basename plus length must not be greater than 15' )) if self.baseName.value.isdigit(): raise Service.ValidationException( _('The machine name can\'t be only numbers')) if int(self.memory.value) < 256 or int( self.memoryGuaranteed.value) < 256: raise Service.ValidationException( _('The minimum allowed memory is 256 Mb')) if int(self.memoryGuaranteed.value) > int(self.memory.value): self.memoryGuaranteed.value = self.memory.value def initGui(self): ''' Loads required values inside ''' # Here we have to use "default values", cause values aren't used at form initialization # This is that value is always '', so if we want to change something, we have to do it # at defValue self.ov.defValue = self.parent().serialize() self.ev.defValue = self.parent().env().key() machines = self.parent().getMachines() vals = [] for m in machines: vals.append(gui.choiceItem(m['id'], m['name'])) # This is not the same case, values is not the "value" of the field, but # the list of values shown because this is a "ChoiceField" self.machine.setValues(vals) clusters = self.parent().getClusters() vals = [] for c in clusters: vals.append(gui.choiceItem(c['id'], c['name'])) self.cluster.setValues(vals) def datastoreHasSpace(self): # Get storages for that datacenter logger.debug('Checking datastore space for {0}'.format( self.datastore.value)) info = self.parent().getStorageInfo(self.datastore.value) logger.debug('Datastore Info: {0}'.format(info)) availableGB = info['available'] / (1024 * 1024 * 1024) if availableGB < self.minSpaceGB.num(): raise Exception( 'Not enough free space available: (Needs at least {0} GB and there is only {1} GB ' .format(self.minSpaceGB.num(), availableGB)) def sanitizeVmName(self, name): ''' Ovirt only allows machine names with [a-zA-Z0-9_-] ''' import re return re.sub("[^a-zA-Z0-9_-]", "_", name) def makeTemplate(self, name, comments): ''' Invokes makeTemplate from parent provider, completing params Args: name: Name to assign to template (must be previously "sanitized" comments: Comments (UTF-8) to add to template Returns: template Id of the template created Raises an exception if operation fails. ''' # Checks datastore size # Get storages for that datacenter self.datastoreHasSpace() return self.parent().makeTemplate(name, comments, self.machine.value, self.cluster.value, self.datastore.value, self.display.value) def getTemplateState(self, templateId): ''' Invokes getTemplateState from parent provider Args: templateId: templateId to remove Returns nothing Raises an exception if operation fails. ''' return self.parent().getTemplateState(templateId) def deployFromTemplate(self, name, comments, templateId): ''' Deploys a virtual machine on selected cluster from selected template Args: name: Name (sanitized) of the machine comments: Comments for machine templateId: Id of the template to deploy from displayType: 'vnc' or 'spice'. Display to use ad oVirt admin interface memoryMB: Memory requested for machine, in MB guaranteedMB: Minimum memory guaranteed for this machine Returns: Id of the machine being created form template ''' logger.debug('Deploying from template {0} machine {1}'.format( templateId, name)) self.datastoreHasSpace() return self.parent().deployFromTemplate( name, comments, templateId, self.cluster.value, self.display.value, int(self.memory.value), int(self.memoryGuaranteed.value)) def removeTemplate(self, templateId): ''' invokes removeTemplate from parent provider ''' return self.parent().removeTemplate(templateId) def getMachineState(self, machineId): ''' Invokes getMachineState from parent provider (returns if machine is "active" or "inactive" Args: machineId: If of the machine to get state Returns: one of this values: unassigned, down, up, powering_up, powered_down, paused, migrating_from, migrating_to, unknown, not_responding, wait_for_launch, reboot_in_progress, saving_state, restoring_state, suspended, image_illegal, image_locked or powering_down Also can return'unknown' if Machine is not known ''' return self.parent().getMachineState(machineId) def startMachine(self, machineId): ''' Tries to start a machine. No check is done, it is simply requested to oVirt. This start also "resume" suspended/paused machines Args: machineId: Id of the machine Returns: ''' return self.parent().startMachine(machineId) def stopMachine(self, machineId): ''' Tries to start a machine. No check is done, it is simply requested to oVirt Args: machineId: Id of the machine Returns: ''' return self.parent().stopMachine(machineId) def suspendMachine(self, machineId): ''' Tries to start a machine. No check is done, it is simply requested to oVirt Args: machineId: Id of the machine Returns: ''' return self.parent().suspendMachine(machineId) def removeMachine(self, machineId): ''' Tries to delete a machine. No check is done, it is simply requested to oVirt Args: machineId: Id of the machine Returns: ''' return self.parent().removeMachine(machineId) def updateMachineMac(self, machineId, macAddres): ''' Changes the mac address of first nic of the machine to the one specified ''' return self.parent().updateMachineMac(machineId, macAddres) def getMacRange(self): ''' Returns de selected mac range ''' return self.parent().getMacRange() def getBaseName(self): ''' Returns the base name ''' return self.baseName.value def getLenName(self): ''' Returns the length of numbers part ''' return int(self.lenName.value) def getDisplay(self): ''' Returns the selected display type (for created machines, for administration ''' return self.display.value
class WinDomainOsManager(WindowsOsManager): typeName = _('Windows Domain OS Manager') typeType = 'WinDomainManager' typeDescription = _('Os Manager to control windows machines with domain.') iconFile = 'wosmanager.png' # Apart form data from windows os manager, we need also domain and credentials domain = gui.TextField(length=64, label=_('Domain'), order=1, tooltip=_('Domain to join machines to (use FQDN form, Netbios name not supported for most operations)'), required=True) account = gui.TextField(length=64, label=_('Account'), order=2, tooltip=_('Account with rights to add machines to domain'), required=True) password = gui.PasswordField(length=64, label=_('Password'), order=3, tooltip=_('Password of the account'), required=True) ou = gui.TextField(length=128, label=_('OU'), order=4, tooltip=_('Organizational unit where to add machines in domain (check it before using it). i.e.: ou=My Machines,dc=mydomain,dc=local')) grp = gui.TextField(length=64, label=_('Machine Group'), order=7, tooltip=_('Group to which add machines on creation. If empty, no group will be used. (experimental)'), tab=_('Advanced')) removeOnExit = gui.CheckBoxField(label=_('Machine clean'), order=8, tooltip=_('If checked, UDS will try to remove the machine from the domain USING the provided credentials'), tab=_('Advanced'), defvalue=gui.TRUE) serverHint = gui.TextField(length=64, label=_('Server Hint'), order=9, tooltip=_('In case of several AD servers, which one is preferred'), tab=_('Advanced')) ssl = gui.CheckBoxField(label=_('Use SSL'), order=10, tooltip=_('If checked, a ssl connection to Active Directory will be used'), tab=_('Advanced'), defvalue=gui.TRUE) # Inherits base "onLogout" onLogout = WindowsOsManager.onLogout idle = WindowsOsManager.idle _domain: str _ou: str _account: str _pasword: str _group: str _serverHint: str _removeOnExit: str _ssl: str def __init__(self, environment: 'Environment', values: 'Module.ValuesType'): super().__init__(environment, values) if values: if values['domain'] == '': raise osmanagers.OSManager.ValidationException(_('Must provide a domain!')) # if values['domain'].find('.') == -1: # raise osmanagers.OSManager.ValidationException(_('Must provide domain in FQDN')) if values['account'] == '': raise osmanagers.OSManager.ValidationException(_('Must provide an account to add machines to domain!')) if values['account'].find('\\') != -1: raise osmanagers.OSManager.ValidationException(_('DOM\\USER form is not allowed!')) if values['password'] == '': raise osmanagers.OSManager.ValidationException(_('Must provide a password for the account!')) self._domain = values['domain'] self._ou = values['ou'].strip() self._account = values['account'] self._password = values['password'] self._group = values['grp'].strip() self._serverHint = values['serverHint'].strip() self._ssl = 'y' if values['ssl'] else 'n' self._removeOnExit = 'y' if values['removeOnExit'] else 'n' else: self._domain = "" self._ou = "" self._account = "" self._password = "" self._group = "" self._serverHint = "" self._removeOnExit = 'n' self._ssl = 'n' # self._ou = self._ou.replace(' ', ''), do not remove spaces if self._domain != '' and self._ou != '': lpath = 'dc=' + ',dc='.join((s.lower() for s in self._domain.split('.'))) if self._ou.lower().find(lpath) == -1: self._ou += ',' + lpath def __getServerList(self) -> typing.Iterable[typing.Tuple[str, int]]: if self._serverHint != '': yield (self._serverHint, 389) for server in reversed(sorted(dns.resolver.query('_ldap._tcp.' + self._domain, 'SRV'), key=lambda i: i.priority * 10000 + i.weight)): yield (str(server.target)[:-1], server.port) def __connectLdap(self, servers: typing.Optional[typing.Iterable[typing.Tuple[str, int]]] = None) -> typing.Any: """ Tries to connect to LDAP Raises an exception if not found: dns.resolver.NXDOMAIN ldaputil.LDAPError """ if servers is None: servers = self.__getServerList() account = self._account if account.find('@') == -1: account += '@' + self._domain _str = "No servers found" # And if not possible, try using NON-SSL for server in servers: ssl = self._ssl == 'y' port = server[1] if not ssl else -1 try: return ldaputil.connection(account, self._password, server[0], port, ssl=ssl, timeout=10, debug=False) except Exception as e: _str = 'Error: {}'.format(e) raise ldaputil.LDAPError(_str) def __getGroup(self, ldapConnection: typing.Any) -> typing.Optional[str]: base = ','.join(['DC=' + i for i in self._domain.split('.')]) group = ldaputil.escape(self._group) obj: typing.Optional[typing.MutableMapping[str, typing.Any]] try: obj = next(ldaputil.getAsDict(ldapConnection, base, "(&(objectClass=group)(|(cn={0})(sAMAccountName={0})))".format(group), ['dn'], sizeLimit=50)) except StopIteration: obj = None if obj is None: return None return obj['dn'] # Returns the DN def __getMachine(self, ldapConnection, machineName: str) -> typing.Optional[str]: # if self._ou: # base = self._ou # else: base = ','.join(['DC=' + i for i in self._domain.split('.')]) fltr = '(&(objectClass=computer)(sAMAccountName={}$))'.format(ldaputil.escape(machineName)) obj: typing.Optional[typing.MutableMapping[str, typing.Any]] try: obj = next(ldaputil.getAsDict(ldapConnection, base, fltr, ['dn'], sizeLimit=50)) except StopIteration: obj = None if obj is None: return None return obj['dn'] # Returns the DN def readyNotified(self, userService: 'UserService') -> None: # No group to add if self._group == '': return if '.' not in self._domain: logger.info('Adding to a group for a non FQDN domain is not supported') return # The machine is on a AD for sure, and maybe they are not already sync error: typing.Optional[str] = None for s in self.__getServerList(): try: ldapConnection = self.__connectLdap(servers=(s,)) machine = self.__getMachine(ldapConnection, userService.friendly_name) group = self.__getGroup(ldapConnection) # # # Direct LDAP operation "modify", maybe this need to be added to ldaputil? :) # # ldapConnection.modify_s(group, ((ldap.MOD_ADD, 'member', machine),)) # @UndefinedVariable error = None break except dns.resolver.NXDOMAIN: # No domain found, log it and pass logger.warning('Could not find _ldap._tcp.%s', self._domain) log.doLog(userService, log.WARN, "Could not remove machine from domain (_ldap._tcp.{0} not found)".format(self._domain), log.OSMANAGER) except ldap.ALREADY_EXISTS: # @UndefinedVariable # Already added this machine to this group, pass error = None break except ldaputil.LDAPError: logger.exception('Ldap Exception caught') error = "Could not add machine (invalid credentials? for {0})".format(self._account) except Exception as e: error = "Could not add machine {} to group {}: {}".format(userService.friendly_name, self._group, e) # logger.exception('Ldap Exception caught') if error: log.doLog(userService, log.WARN, error, log.OSMANAGER) logger.error(error) def release(self, userService: 'UserService') -> None: super().release(userService) # If no removal requested, just return if self._removeOnExit != 'y': return if '.' not in self._domain: # logger.info('Releasing from a not FQDN domain is not supported') log.doLog(userService, log.INFO, "Removing a domain machine form a non FQDN domain is not supported.", log.OSMANAGER) return try: ldapConnection = self.__connectLdap() except dns.resolver.NXDOMAIN: # No domain found, log it and pass logger.warning('Could not find _ldap._tcp.%s', self._domain) log.doLog(userService, log.WARN, "Could not remove machine from domain (_ldap._tcp.{} not found)".format(self._domain), log.OSMANAGER) return except ldaputil.LDAPError as e: # logger.exception('Ldap Exception caught') log.doLog(userService, log.WARN, "Could not remove machine from domain ({})".format(e), log.OSMANAGER) return except Exception as e: # logger.exception('Exception caught') log.doLog(userService, log.WARN, "Could not remove machine from domain ({})".format(e), log.OSMANAGER) return try: res = self.__getMachine(ldapConnection, userService.friendly_name) if res is None: raise Exception('Machine {} not found on AD (permissions?)'.format(userService.friendly_name)) ldaputil.recursive_delete(ldapConnection, res) except IndexError: logger.error('Error deleting %s from BASE %s', userService.friendly_name, self._ou) except Exception: logger.exception('Deleting from AD: ') def check(self) -> str: try: ldapConnection = self.__connectLdap() except ldaputil.LDAPError as e: return _('Check error: {}').format(e) except dns.resolver.NXDOMAIN: return _('Could not find server parameters (_ldap._tcp.{0} can\'t be resolved)').format(self._domain) except Exception as e: logger.exception('Exception ') return str(e) try: ldapConnection.search_st(self._ou, ldap.SCOPE_BASE) # @UndefinedVariable except ldaputil.LDAPError as e: return _('Check error: {}').format(e) # Group if self._group != '': if self.__getGroup(ldapConnection) is None: return _('Check Error: group "{}" not found (using "cn" to locate it)').format(self._group) return _('Server check was successful') # pylint: disable=protected-access @staticmethod def test(env: 'Environment', data: typing.Dict[str, str]) -> typing.List[typing.Any]: logger.debug('Test invoked') try: wd: WinDomainOsManager = WinDomainOsManager(env, data) logger.debug(wd) try: ldapConnection = wd.__connectLdap() except ldaputil.LDAPError as e: return [False, _('Could not access AD using LDAP ({0})').format(e)] ou = wd._ou if ou == '': ou = 'cn=Computers,dc=' + ',dc='.join(wd._domain.split('.')) logger.info('Checking %s with ou %s', wd._domain, ou) r = ldapConnection.search_st(ou, ldap.SCOPE_BASE) # @UndefinedVariable logger.info('Result of search: %s', r) except ldaputil.LDAPError: if not wd._ou: return [False, _('The default path {0} for computers was not found!!!').format(wd._ou)] return [False, _('The ou path {0} was not found!!!').format(wd._ou)] except dns.resolver.NXDOMAIN: return [True, _('Could not check parameters (_ldap._tcp.{0} can\'r be resolved)').format(wd._domain)] except Exception as e: logger.exception('Exception ') return [False, str(e)] return [True, _("All parameters seem to work fine.")] def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]: return { 'action': 'rename_ad', 'name': userService.getName(), 'ad': self._domain, 'ou': self._ou, 'username': self._account, 'password': self._password, } def infoVal(self, userService: 'UserService') -> str: return 'domain:{0}\t{1}\t{2}\t{3}\t{4}'.format(self.getName(userService), self._domain, self._ou, self._account, self._password) def infoValue(self, userService: 'UserService') -> str: return 'domain\r{0}\t{1}\t{2}\t{3}\t{4}'.format(self.getName(userService), self._domain, self._ou, self._account, self._password) def marshal(self) -> bytes: ''' Serializes the os manager data so we can store it in database ''' base = typing.cast(str, encoders.encode(super().marshal(), 'hex', asText=True)) return '\t'.join([ 'v4', self._domain, self._ou, self._account, cryptoManager().encrypt(self._password), base, self._group, self._serverHint, self._ssl, self._removeOnExit ]).encode('utf8') def unmarshal(self, data: bytes) -> None: values = data.decode('utf8').split('\t') if values[0] in ('v1', 'v2', 'v3', 'v4'): self._domain = values[1] self._ou = values[2] self._account = values[3] self._password = cryptoManager().decrypt(values[4]) if values[0] in ('v2', 'v3', 'v4'): self._group = values[6] else: self._group = '' if values[0] in ('v3', 'v4'): self._serverHint = values[7] else: self._serverHint = '' if values[0] == 'v4': self._ssl = values[8] self._removeOnExit = values[9] else: self._ssl = 'n' self._removeOnExit = 'y' super().unmarshal(typing.cast(bytes, encoders.decode(values[5], 'hex'))) def valuesDict(self) -> gui.ValuesDictType: dct = super().valuesDict() dct['domain'] = self._domain dct['ou'] = self._ou dct['account'] = self._account dct['password'] = self._password dct['grp'] = self._group dct['serverHint'] = self._serverHint dct['ssl'] = self._ssl == 'y' dct['removeOnExit'] = self._removeOnExit == 'y' return dct
class IPMachinesService(IPServiceBase): # Gui token = gui.TextField( order=1, label=_('Service Token'), length=16, tooltip= _('Service token that will be used by actors to communicate with service. Leave empty for persistent assignation.' ), defvalue='', required=False, rdonly=False, ) ipList = gui.EditableList( label=_('List of servers'), tooltip=_('List of servers available for this service'), ) port = gui.NumericField( length=5, label=_('Check Port'), defvalue='0', order=2, tooltip= _('If non zero, only hosts responding to connection on that port will be served.' ), required=True, tab=gui.ADVANCED_TAB, ) skipTimeOnFailure = gui.NumericField( length=6, label=_('Skip time'), defvalue='0', order=2, tooltip=_( 'If a host fails to check, skip it for this time (in minutes).'), minValue=0, required=True, tab=gui.ADVANCED_TAB, ) # Description of service typeName = _('Static Multiple IP') typeType = 'IPMachinesService' typeDescription = _( 'This service provides access to POWERED-ON Machines by IP') iconFile = 'machines.png' # Characteristics of service maxDeployed = ( -1 ) # If the service provides more than 1 "provided service" (-1 = no limit, 0 = ???? (do not use it!!!), N = max number to deploy usesCache = False # Cache are running machine awaiting to be assigned usesCache_L2 = False # L2 Cache are running machines in suspended state needsManager = False # If the service needs a s.o. manager (managers are related to agents provided by services itselfs, i.e. virtual machines with agent) mustAssignManually = False # If true, the system can't do an automatic assignation of a deployed user service from this service deployedType = IPMachineDeployed servicesTypeProvided = (serviceTypes.VDI, ) _ips: typing.List[str] = [] _token: str = '' _port: int = 0 _skipTimeOnFailure: int = 0 def initialize(self, values: 'Module.ValuesType') -> None: if values is None: return if values.get('ipList', None) is None: self._ips = [] else: # Check that ips are valid for v in values['ipList']: if not net.isValidHost(v): raise IPServiceBase.ValidationException( gettext('Invalid value detected on servers list: "{}"' ).format(v)) self._ips = [ '{}~{}'.format(str(ip).strip(), i) for i, ip in enumerate(values['ipList']) if str(ip).strip() ] # Allow duplicates right now # Current stored data, if it exists d = self.storage.readData('ips') old_ips = pickle.loads(d) if d and isinstance(d, bytes) else [] # dissapeared ones dissapeared = set(i.split('~')[0] for i in old_ips) - set( i.split('~')[0] for i in self._ips) with transaction.atomic(): for removable in dissapeared: self.storage.remove(removable) self._token = self.token.value.strip() self._port = self.port.value self._skipTimeOnFailure = self.skipTimeOnFailure.num() def getToken(self): return self._token or None def valuesDict(self) -> gui.ValuesDictType: ips = (i.split('~')[0] for i in self._ips) return { 'ipList': gui.convertToList(ips), 'token': self._token, 'port': str(self._port), 'skipTimeOnFailure': str(self._skipTimeOnFailure), } def marshal(self) -> bytes: self.storage.saveData('ips', pickle.dumps(self._ips)) return b'\0'.join([ b'v4', self._token.encode(), str(self._port).encode(), str(self._skipTimeOnFailure).encode(), ]) def unmarshal(self, data: bytes) -> None: values: typing.List[bytes] = data.split(b'\0') d = self.storage.readData('ips') if isinstance(d, bytes): self._ips = pickle.loads(d) elif isinstance(d, str): # "legacy" saved elements self._ips = pickle.loads(d.encode('utf8')) self.marshal() # Ensure now is bytes.. else: self._ips = [] if values[0] != b'v1': self._token = values[1].decode() if values[0] in (b'v3', b'v4'): self._port = int(values[2].decode()) if values[0] == b'v4': self._skipTimeOnFailure = int(values[3].decode()) # Sets maximum services for this self.maxDeployed = len(self._ips) def getUnassignedMachine(self) -> typing.Optional[str]: # Search first unassigned machine try: now = getSqlDatetimeAsUnix() consideredFreeTime = now - config.GlobalConfig.SESSION_EXPIRE_TIME.getInt( force=False) * 3600 for ip in self._ips: theIP = IPServiceBase.getIp(ip) theMAC = IPServiceBase.getMac(ip) locked = self.storage.getPickle(theIP) if not locked or locked < consideredFreeTime: if self._port > 0 and self._skipTimeOnFailure > 0 and self.cache.get( 'port{}'.format(theIP)): continue # The check failed not so long ago, skip it... self.storage.putPickle(theIP, now) # Is WOL enabled? wolENABLED = bool(self.parent().wolURL(theIP, theMAC)) # Now, check if it is available on port, if required... if self._port > 0 and not wolENABLED: # If configured WOL, check is a nonsense if (connection.testServer( theIP, self._port, timeOut=0.5) is False): # Log into logs of provider, so it can be "shown" on services logs self.parent().doLog( log.WARN, 'Host {} not accesible on port {}'.format( theIP, self._port), ) logger.warning( 'Static Machine check on %s:%s failed. Will be ignored for %s minutes.', theIP, self._port, self._skipTimeOnFailure, ) self.storage.remove( theIP) # Return Machine to pool if self._skipTimeOnFailure > 0: self.cache.put( 'port{}'.format(theIP), '1', validity=self._skipTimeOnFailure * 60, ) continue if theMAC: return theIP + ';' + theMAC return theIP return None except Exception: logger.exception("Exception at getUnassignedMachine") return None def unassignMachine(self, ip: str) -> None: try: if ';' in ip: ip = ip.split(';')[0] # ; means that HAS an attached MAC self.storage.remove(ip) except Exception: logger.exception("Exception at getUnassignedMachine") def listAssignables(self): return [(ip, ip.split('~')[0]) for ip in self._ips if self.storage.readData(ip) is None] def assignFromAssignables( self, assignableId: str, user: '******', userDeployment: 'services.UserDeployment', ) -> str: userServiceInstance: IPMachineDeployed = typing.cast( IPMachineDeployed, userDeployment) theIP = IPServiceBase.getIp(assignableId) if self.storage.readData(theIP) is None: self.storage.saveData(theIP, theIP) return userServiceInstance.assign(theIP) return userServiceInstance.error('IP already assigned')
class OGProvider(ServiceProvider): ''' This class represents the sample services provider In this class we provide: * The Provider functionality * The basic configuration parameters for the provider * The form fields needed by administrators to configure this provider :note: At class level, the translation must be simply marked as so using ugettext_noop. This is so cause we will translate the string when sent to the administration client. For this class to get visible at administration client as a provider type, we MUST register it at package __init__. ''' # : What kind of services we offer, this are classes inherited from Service offers = [OGService] # : Name to show the administrator. This string will be translated BEFORE # : sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) typeName = _('OpenGnsys Platform Provider') # : Type used internally to identify this provider typeType = 'openGnsysPlatform' # : Description shown at administration interface for this provider typeDescription = _('OpenGnsys platform service provider (experimental)') # : Icon file used as icon for this provider. This string will be translated # : BEFORE sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) iconFile = 'provider.png' # now comes the form fields # There is always two fields that are requested to the admin, that are: # Service Name, that is a name that the admin uses to name this provider # Description, that is a short description that the admin gives to this provider # Now we are going to add a few fields that we need to use this provider # Remember that these are "dummy" fields, that in fact are not required # but used for sample purposes # If we don't indicate an order, the output order of fields will be # "random" host = gui.TextField(length=64, label=_('Host'), order=1, tooltip=_('OpenGnsys Host'), required=True) port = gui.NumericField(length=5, label=_('Port'), defvalue='443', order=2, tooltip=_('OpenGnsys Port (default is 443, and only ssl connection is allowed)'), required=True) checkCert = gui.CheckBoxField(label=_('Check Cert.'), order=3, tooltip=_('If checked, ssl certificate of OpenGnsys server must be valid (not self signed)')) username = gui.TextField(length=32, label=_('Username'), order=4, tooltip=_('User with valid privileges on OpenGnsys'), required=True) password = gui.PasswordField(lenth=32, label=_('Password'), order=5, tooltip=_('Password of the user of OpenGnsys'), required=True) udsServerAccessUrl = gui.TextField(length=32, label=_('UDS Server URL'), order=6, tooltip=_('URL used by OpenGnsys to access UDS. If empty, UDS will guess it.'), required=False, tab=gui.PARAMETERS_TAB) maxPreparingServices = gui.NumericField(length=3, label=_('Creation concurrency'), defvalue='10', minValue=1, maxValue=65536, order=50, tooltip=_('Maximum number of concurrently creating VMs'), required=True, tab=gui.ADVANCED_TAB) maxRemovingServices = gui.NumericField(length=3, label=_('Removal concurrency'), defvalue='5', minValue=1, maxValue=65536, order=51, tooltip=_('Maximum number of concurrently removing VMs'), required=True, tab=gui.ADVANCED_TAB) timeout = gui.NumericField(length=3, label=_('Timeout'), defvalue='10', order=90, tooltip=_('Timeout in seconds of connection to OpenGnsys'), required=True, tab=gui.ADVANCED_TAB) # Own variables _api = None def initialize(self, values=None): ''' We will use the "autosave" feature for form fields ''' # Just reset _api connection variable self._api = None if values is not None: self.timeout.value = validators.validateTimeout(self.timeout.value, returnAsInteger=False) logger.debug('Endpoint: {}'.format(self.endpoint)) try: request = values['_request'] if self.udsServerAccessUrl.value.strip() == '': self.udsServerAccessUrl.value = request.build_absolute_uri('/') if self.udsServerAccessUrl.value[-1] != '/': self.udsServerAccessUrl.value += '/' except Exception: pass @property def endpoint(self): return 'https://{}:{}/opengnsys/rest'.format(self.host.value, self.port.value) @property def api(self): if self._api is None: self._api = og.OpenGnsysClient(self.username.value, self.password.value, self.endpoint, self.cache, self.checkCert.isTrue()) logger.debug('Api: {}'.format(self._api)) return self._api def resetApi(self): self._api = None def testConnection(self): ''' Test that conection to OpenGnsys server is fine Returns True if all went fine, false if id didn't ''' try: if self.api.version[0:5] < '1.1.0': return [False, 'OpenGnsys version is not supported (required version 1.1.0 or newer and found {})'.format(self.api.version)] except Exception as e: logger.exception('Error') return [False, '{}'.format(e)] return [True, _('OpenGnsys test connection passed')] @staticmethod def test(env, data): ''' Test ovirt Connectivity Args: env: environment passed for testing (temporal environment passed) data: data passed for testing (data obtained from the form definition) Returns: Array of two elements, first is True of False, depending on test (True is all right, false is error), second is an String with error, preferably i18n.. ''' return OGProvider(env, data).testConnection() def getUDSServerAccessUrl(self): return self.udsServerAccessUrl.value def reserve(self, ou, image, lab=0, maxtime=0): return self.api.reserve(ou, image, lab, maxtime) def unreserve(self, machineId): return self.api.unreserve(machineId) def notifyEvents(self, machineId, loginURL, logoutURL): return self.api.notifyURLs(machineId, loginURL, logoutURL) def notifyDeadline(self, machineId, deadLine): return self.api.notifyDeadline(machineId, deadLine) def status(self, machineId): return self.api.status(machineId)
class TSPICETransport(BaseSpiceTransport): """ Provides access via SPICE to service. This transport can use an domain. If username processed by authenticator contains '@', it will split it and left-@-part will be username, and right password """ typeName = _('SPICE') typeType = 'TSSPICETransport' typeDescription = _('SPICE Protocol. Tunneled connection.') protocol = transports.protocols.SPICE group: typing.ClassVar[str] = transports.TUNNELED_GROUP tunnelServer = gui.TextField( label=_('Tunnel server'), order=1, tooltip= _('IP or Hostname of tunnel server sent to client device ("public" ip) and port. (use HOST:PORT format)' ), tab=gui.TUNNEL_TAB) serverCertificate = BaseSpiceTransport.serverCertificate fullScreen = BaseSpiceTransport.fullScreen usbShare = BaseSpiceTransport.usbShare autoNewUsbShare = BaseSpiceTransport.autoNewUsbShare smartCardRedirect = BaseSpiceTransport.smartCardRedirect def initialize(self, values: 'Module.ValuesType'): if values: if values['tunnelServer'].count(':') != 1: raise transports.Transport.ValidationException( _('Must use HOST:PORT in Tunnel Server Field')) def getUDSTransportScript( # pylint: disable=too-many-locals self, userService: 'models.UserService', transport: 'models.Transport', ip: str, os: typing.Dict[str, str], user: '******', password: str, request: 'HttpRequest' ) -> typing.Tuple[str, str, typing.Dict[str, typing.Any]]: userServiceInstance: typing.Any = userService.getInstance() # Spice connection con = userServiceInstance.getConsoleConnection() port: str = con['port'] or '-1' secure_port: str = con['secure_port'] or '-1' # Ticket tunpass = ''.join(random.SystemRandom().choice(string.letters + string.digits) for _i in range(12)) tunuser = TicketStore.create(tunpass) sshHost, sshPort = self.tunnelServer.value.split(':') r = RemoteViewerFile('127.0.0.1', '{port}', '{secure_port}', con['ticket']['value'], self.serverCertificate.value.strip(), con['cert_subject'], fullscreen=self.fullScreen.isTrue()) r.usb_auto_share = self.usbShare.isTrue() r.new_usb_auto_share = self.autoNewUsbShare.isTrue() r.smartcard = self.smartCardRedirect.isTrue() osName = { OsDetector.Windows: 'windows', OsDetector.Linux: 'linux', OsDetector.Macintosh: 'macosx' }.get(os['OS']) if osName is None: return super().getUDSTransportScript(userService, transport, ip, os, user, password, request) # if sso: # If SSO requested, and when supported by platform # userServiceInstance.desktopLogin(user, password, '') sp = { 'as_file': r.as_file, 'as_file_ns': r.as_file_ns, 'tunUser': tunuser, 'tunPass': tunpass, 'tunHost': sshHost, 'tunPort': sshPort, 'ip': con['address'], 'port': port, 'secure_port': secure_port } return self.getScript('scripts/{}/tunnel.py', osName, sp)
class Provider(ServiceProvider): """ This class represents the sample services provider In this class we provide: * The Provider functionality * The basic configuration parameters for the provider * The form fields needed by administrators to configure this provider :note: At class level, the translation must be simply marked as so using ugettext_noop. This is so cause we will translate the string when sent to the administration client. For this class to get visible at administration client as a provider type, we MUST register it at package __init__. """ # : What kind of services we offer, this are classes inherited from Service offers = [ServiceOne, ServiceTwo] # : Name to show the administrator. This string will be translated BEFORE # : sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) typeName = _('Sample Provider') # : Type used internally to identify this provider typeType = 'SampleProvider' # : Description shown at administration interface for this provider typeDescription = _('Sample (and dummy) service provider') # : Icon file used as icon for this provider. This string will be translated # : BEFORE sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) iconFile = 'provider.png' # now comes the form fields # There is always two fields that are requested to the admin, that are: # Service Name, that is a name that the admin uses to name this provider # Description, that is a short description that the admin gives to this provider # Now we are going to add a few fields that we need to use this provider # Remember that these are "dummy" fields, that in fact are not required # but used for sample purposes # If we don't indicate an order, the output order of fields will be # "random" # : Remote host. Here core will translate label and tooltip, remember to # : mark them as _ using ugettext_noop. remoteHost = gui.TextField( oder=1, length=64, label=_('Remote host'), tooltip=_('This fields contains a remote host'), required=True, ) # : Name of your pet (sample, not really needed :-) ) petName = gui.TextField( order=2, length=32, label=_('Your pet\'s name'), tooltip=_('If you like, write the name of your pet'), requred=False, defvalue='Tux' # : This will not get translated ) # : Age of Methuselah (matusalén in spanish) # : in Spain there is a well-known to say that something is very old, # : "Tiene mas años que matusalén"(is older than Methuselah) methAge = gui.NumericField( order=3, length=4, # That is, max allowed value is 9999 label=_('Age of Methuselah'), tooltip=_('If you know it, please, tell me!!!'), required= True, # : Numeric fields have always a value, so this not really needed defvalue='4500') # : Is Methuselah istill alive? methAlive = gui.CheckBoxField( order=4, label=_('Is Methuselah still alive?'), tooltip=_('If you fail, this will not get saved :-)'), required=True, # : Also means nothing. Check boxes has always a value defvalue=gui.TRUE # : By default, at new item, check this ) methText = gui.TextField( order=5, length=512, multiline=5, label=_('Text area'), tooltip=_('This is a text area'), requred=False, defvalue='Write\nsomething' # : This will not get translated ) # There is more fields type, but not here the best place to cover it def initialize(self, values=None): """ We will use the "autosave" feature for form fields, that is more than enought for most providers. (We simply need to store data provided by user and, maybe, initialize some kind of connection with this values). Normally provider values are rally used at sevice level, cause we never instantiate nothing except a service from a provider. """ # If you say meth is alive, you are wrong!!! (i guess..) # values are only passed from administration client. Internals # instantiations are always empty. if values is not None and self.methAlive.isTrue(): raise ServiceProvider.ValidationException( _('Methuselah is not alive!!! :-)')) # Marshal and unmarshal are defaults ones, also enought # As we use "autosave" fields feature, dictValues is also provided by # base class so we don't have to mess with all those things... @staticmethod def test(env, data): """ Create your test method here so the admin can push the "check" button and this gets executed. Args: env: environment passed for testing (temporal environment passed) data: data passed for testing (data obtained from the form definition) Returns: Array of two elements, first is True of False, depending on test (True is all right, false is error), second is an String with error, preferably internacionalizated.. In this case, wi well do nothing more that use the provider params Note also that this is an static method, that will be invoked using the admin user provided data via administration client, and a temporary environment that will be erased after invoking this method """ try: # We instantiate the provider, but this may fail... instance = Provider(env, data) logger.debug('Methuselah has {0} years and is {1} :-)'.format( instance.methAge.value, instance.methAlive.value)) except ServiceProvider.ValidationException as e: # If we say that meth is alive, instantiation will return [False, str(e)] except Exception as e: logger.exception("Exception caugth!!!") return [False, str(e)] return [True, _('Nothing tested, but all went fine..')] # Congratulations!!!, the needed part of your first simple provider is done! # Now you can go to administration panel, and check it # # From now onwards, we implement our own methods, that will be used by, # for example, services derived from this provider def host(self): """ Sample method, in fact in this we just return the value of host field, that is an string """ return self.remoteHost.value def methYears(self): """ Another sample return, it will in fact return the Methuselah years """ return self.methAge.value()
class Provider(ServiceProvider): """ This class represents the sample services provider In this class we provide: * The Provider functionality * The basic configuration parameters for the provider * The form fields needed by administrators to configure this provider :note: At class level, the translation must be simply marked as so using ugettext_noop. This is so cause we will translate the string when sent to the administration client. For this class to get visible at administration client as a provider type, we MUST register it at package __init__. """ # : What kind of services we offer, this are classes inherited from Service offers = [LiveService] # : Name to show the administrator. This string will be translated BEFORE # : sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) typeName = _('OpenStack Platform Provider') # : Type used internally to identify this provider typeType = 'openStackPlatform' # : Description shown at administration interface for this provider typeDescription = _('OpenStack platform service provider') # : Icon file used as icon for this provider. This string will be translated # : BEFORE sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) iconFile = 'provider.png' # now comes the form fields # There is always two fields that are requested to the admin, that are: # Service Name, that is a name that the admin uses to name this provider # Description, that is a short description that the admin gives to this provider # Now we are going to add a few fields that we need to use this provider # Remember that these are "dummy" fields, that in fact are not required # but used for sample purposes # If we don't indicate an order, the output order of fields will be # "random" host = gui.TextField(length=64, label=_('Host'), order=1, tooltip=_('OpenStack Host'), required=True) port = gui.NumericField(length=5, label=_('Port'), defvalue='5000', order=2, tooltip=_('OpenStack Port'), required=True) ssl = gui.CheckBoxField( label=_('Use SSL'), order=3, tooltip= _('If checked, the connection will be forced to be ssl (will not work if server is not providing ssl)' )) access = gui.ChoiceField(label=_('Access interface'), order=4, tooltip=_('Access interface to be used'), values=INTERFACE_VALUES, defvalue='public') domain = gui.TextField(length=64, label=_('Domain'), order=8, tooltip=_('Domain name (default is Default)'), required=True, defvalue='Default') username = gui.TextField( length=64, label=_('Username'), order=9, tooltip=_('User with valid privileges on OpenStack'), required=True, defvalue='admin') password = gui.PasswordField( lenth=32, label=_('Password'), order=10, tooltip=_('Password of the user of OpenStack'), required=True) maxPreparingServices = gui.NumericField( length=3, label=_('Creation concurrency'), defvalue='10', minValue=1, maxValue=65536, order=50, tooltip=_('Maximum number of concurrently creating VMs'), required=True, tab=gui.ADVANCED_TAB) maxRemovingServices = gui.NumericField( length=3, label=_('Removal concurrency'), defvalue='5', minValue=1, maxValue=65536, order=51, tooltip=_('Maximum number of concurrently removing VMs'), required=True, tab=gui.ADVANCED_TAB) timeout = gui.NumericField( length=3, label=_('Timeout'), defvalue='10', minValue=1, maxValue=128, order=99, tooltip=_('Timeout in seconds of connection to OpenStack'), required=True, tab=gui.ADVANCED_TAB) # tenant = gui.TextField(length=64, label=_('Project'), order=6, tooltip=_('Project (tenant) for this provider'), required=True, defvalue='') # region = gui.TextField(length=64, label=_('Region'), order=7, tooltip=_('Region for this provider'), required=True, defvalue='RegionOne') # Own variables _api = None def initialize(self, values=None): """ We will use the "autosave" feature for form fields """ # Just reset _api connection variable if values is not None: self.timeout.value = validators.validateTimeout( self.timeout.value, returnAsInteger=False) def api(self, projectId=None, region=None): return openStack.Client(self.host.value, self.port.value, self.domain.value, self.username.value, self.password.value, useSSL=self.ssl.isTrue(), projectId=projectId, region=region, access=self.access.value) def sanitizeVmName(self, name): return openStack.sanitizeName(name) def testConnection(self): """ Test that conection to OpenStack server is fine Returns True if all went fine, false if id didn't """ try: if self.api().testConnection() is False: raise Exception('Check connection credentials, server, etc.') except Exception as e: return [False, '{}'.format(e)] return [True, _('OpenStack test connection passed')] @staticmethod def test(env, data): """ Test ovirt Connectivity Args: env: environment passed for testing (temporal environment passed) data: data passed for testing (data obtained from the form definition) Returns: Array of two elements, first is True of False, depending on test (True is all right, false is error), second is an String with error, preferably internacionalizated.. """ return Provider(env, data).testConnection()
class LiveService(Service): """ Opennebula Live Service """ # : Name to show the administrator. This string will be translated BEFORE # : sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) typeName = _('OpenNebula Live Images') # : Type used internally to identify this provider typeType = 'openNebulaLiveService' # : Description shown at administration interface for this provider typeDescription = _('OpenNebula live images based service') # : Icon file used as icon for this provider. This string will be translated # : BEFORE sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) iconFile = 'provider.png' # Functional related data # : If the service provides more than 1 "deployed user" (-1 = no limit, # : 0 = ???? (do not use it!!!), N = max number to deploy maxDeployed = -1 # : If we need to generate "cache" for this service, so users can access the # : provided services faster. Is usesCache is True, you will need also # : set publicationType, do take care about that! usesCache = True # : Tooltip shown to user when this item is pointed at admin interface, none # : because we don't use it cacheTooltip = _( 'Number of desired machines to keep running waiting for an user') # : If the service needs a s.o. manager (managers are related to agents # : provided by services itselfs, i.e. virtual machines with actors) needsManager = True # : If true, the system can't do an automatic assignation of a deployed user # : service from this service mustAssignManually = False canReset = True # : Types of publications (preparated data for deploys) # : In our case, we do no need a publication, so this is None publicationType = LivePublication # : Types of deploys (services in cache and/or assigned to users) deployedType = LiveDeployment allowedProtocols = protocols.GENERIC + (protocols.SPICE, ) servicesTypeProvided = (serviceTypes.VDI, ) # Now the form part datastore = gui.ChoiceField(label=_("Datastore"), order=100, tooltip=_('Service clones datastore'), required=True) template = gui.ChoiceField(label=_("Base Template"), order=110, tooltip=_('Service base template'), tab=_('Machine'), required=True) baseName = gui.TextField( label=_('Machine Names'), rdonly=False, order=111, tooltip=_('Base name for clones from this machine'), tab=_('Machine'), required=True) lenName = gui.NumericField( length=1, label=_('Name Length'), defvalue=5, order=112, tooltip=_('Size of numeric part for the names of these machines'), tab=_('Machine'), required=True) def initialize(self, values: 'Module.ValuesType') -> None: """ We check here form values to see if they are valid. Note that we check them throught FROM variables, that already has been initialized by __init__ method of base class, before invoking this. """ if not values: return self.baseName.value = validators.validateHostname(self.baseName.value, maxLength=15 - self.lenName.num(), asPattern=True) def parent(self) -> 'OpenNebulaProvider': return typing.cast('OpenNebulaProvider', super().parent()) def initGui(self) -> None: """ Loads required values inside """ t: 'on.types.TemplateType' self.template.setValues([ gui.choiceItem(t.id, t.name) for t in self.parent().getTemplates() ]) d: 'on.types.StorageType' self.datastore.setValues([ gui.choiceItem(d.id, d.name) for d in self.parent().getDatastores() ]) def sanitizeVmName(self, name: str) -> str: return self.parent().sanitizeVmName(name) def makeTemplate(self, templateName: str) -> str: return self.parent().makeTemplate(self.template.value, templateName, self.datastore.value) def checkTemplatePublished(self, templateId: str) -> bool: return self.parent().checkTemplatePublished(templateId) def deployFromTemplate(self, name: str, templateId: str) -> str: """ Deploys a virtual machine on selected cluster from selected template Args: name: Name (sanitized) of the machine comments: Comments for machine templateId: Id of the template to deploy from displayType: 'vnc' or 'spice'. Display to use ad OpenNebula admin interface memoryMB: Memory requested for machine, in MB guaranteedMB: Minimum memory guaranteed for this machine Returns: Id of the machine being created form template """ logger.debug('Deploying from template %s machine %s', templateId, name) # self.datastoreHasSpace() return self.parent().deployFromTemplate(name, templateId) def removeTemplate(self, templateId: str) -> None: """ invokes removeTemplate from parent provider """ self.parent().removeTemplate(templateId) def getMachineState(self, machineId: str) -> 'on.types.VmState': """ Invokes getMachineState from parent provider (returns if machine is "active" or "inactive" Args: machineId: If of the machine to get state Returns: one of this values: unassigned, down, up, powering_up, powered_down, paused, migrating_from, migrating_to, unknown, not_responding, wait_for_launch, reboot_in_progress, saving_state, restoring_state, suspended, image_illegal, image_locked or powering_down Also can return'unknown' if Machine is not known """ return self.parent().getMachineState(machineId) def getMachineSubstate(self, machineId: str) -> int: """ On OpenNebula, the machine can be "active" but not "running". Any active machine will have a LCM_STATE, that is what we get here """ return self.parent().getMachineSubstate(machineId) def startMachine(self, machineId: str) -> None: """ Tries to start a machine. No check is done, it is simply requested to OpenNebula. This start also "resume" suspended/paused machines Args: machineId: Id of the machine Returns: """ self.parent().startMachine(machineId) def stopMachine(self, machineId: str) -> None: """ Tries to stop a machine. No check is done, it is simply requested to OpenNebula Args: machineId: Id of the machine Returns: """ self.parent().stopMachine(machineId) def suspendMachine(self, machineId: str) -> None: """ Tries to suspend machine. No check is done, it is simply requested to OpenNebula Args: machineId: Id of the machine Returns: """ self.parent().suspendMachine(machineId) def resetMachine(self, machineId: str) -> None: self.parent().resetMachine(machineId) def removeMachine(self, machineId: str) -> None: """ Tries to delete a machine. No check is done, it is simply requested to OpenNebula Args: machineId: Id of the machine Returns: """ self.parent().removeMachine(machineId) def getNetInfo( self, machineId: str, networkId: typing.Optional[str] = None) -> typing.Tuple[str, str]: """ Changes the mac address of first nic of the machine to the one specified """ return self.parent().getNetInfo(machineId, networkId=None) def getBaseName(self) -> str: """ Returns the base name """ return self.baseName.value def getLenName(self) -> int: """ Returns the length of numbers part """ return self.lenName.num() def getConsoleConnection(self, machineId: str) -> typing.Dict[str, typing.Any]: return self.parent().getConsoleConnection(machineId) def desktopLogin(self, machineId: str, username: str, password: str, domain: str) -> typing.Dict[str, typing.Any]: return self.parent().desktopLogin(machineId, username, password, domain)
class XenProvider(ServiceProvider): # pylint: disable=too-many-public-methods """ This class represents the sample services provider In this class we provide: * The Provider functionality * The basic configuration parameters for the provider * The form fields needed by administrators to configure this provider :note: At class level, the translation must be simply marked as so using ugettext_noop. This is so cause we will translate the string when sent to the administration client. For this class to get visible at administration client as a provider type, we MUST register it at package __init__. """ # : What kind of services we offer, this are classes inherited from Service offers = [XenLinkedService] # : Name to show the administrator. This string will be translated BEFORE # : sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) typeName = _('Xenserver/XCP-NG Platforms Provider') # : Type used internally to identify this provider typeType = 'XenPlatform' # : Description shown at administration interface for this provider typeDescription = _('XenServer and XCP-NG platforms service provider') # : Icon file used as icon for this provider. This string will be translated # : BEFORE sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) iconFile = 'provider.png' # now comes the form fields # There is always two fields that are requested to the admin, that are: # Service Name, that is a name that the admin uses to name this provider # Description, that is a short description that the admin gives to this provider # Now we are going to add a few fields that we need to use this provider # Remember that these are "dummy" fields, that in fact are not required # but used for sample purposes # If we don't indicate an order, the output order of fields will be # "random" host = gui.TextField(length=64, label=_('Host'), order=1, tooltip=_('XenServer Server IP or Hostname'), required=True) username = gui.TextField( length=32, label=_('Username'), order=2, tooltip=_('User with valid privileges on XenServer'), required=True, defvalue='root') password = gui.PasswordField( lenth=32, label=_('Password'), order=3, tooltip=_('Password of the user of XenServer'), required=True) maxPreparingServices = gui.NumericField( length=3, label=_('Creation concurrency'), defvalue='10', minValue=1, maxValue=65536, order=50, tooltip=_('Maximum number of concurrently creating VMs'), required=True, tab=gui.ADVANCED_TAB) maxRemovingServices = gui.NumericField( length=3, label=_('Removal concurrency'), defvalue='5', minValue=1, maxValue=65536, order=51, tooltip=_('Maximum number of concurrently removing VMs'), required=True, tab=gui.ADVANCED_TAB) macsRange = gui.TextField( length=36, label=_('Macs range'), defvalue='02:46:00:00:00:00-02:46:00:FF:FF:FF', order=90, rdonly=True, tooltip=_('Range of valid macs for created machines'), required=True, tab=gui.ADVANCED_TAB) verifySSL = gui.CheckBoxField( label=_('Verify Certificate'), order=91, tooltip= _('If selected, certificate will be checked against system valid certificate providers' ), required=True, tab=gui.ADVANCED_TAB, defvalue=gui.FALSE) _api: typing.Optional[XenServer] # XenServer engine, right now, only permits a connection to one server and only one per instance # If we want to connect to more than one server, we need keep locked access to api, change api server, etc.. # We have implemented an "exclusive access" client that will only connect to one server at a time (using locks) # and this way all will be fine def __getApi(self, force: bool = False) -> XenServer: """ Returns the connection API object for XenServer (using XenServersdk) """ if not self._api or force: self._api = XenServer(self.host.value, 443, self.username.value, self.password.value, True, self.verifySSL.isTrue()) return self._api # There is more fields type, but not here the best place to cover it def initialize(self, values: 'Module.ValuesType') -> None: """ We will use the "autosave" feature for form fields """ # Just reset _api connection variable self._api = None def testConnection(self): """ Test that conection to XenServer server is fine Returns True if all went fine, false if id didn't """ self.__getApi().test() def checkTaskFinished( self, task: typing.Optional[str]) -> typing.Tuple[bool, str]: """ Checks a task state. Returns None if task is Finished Returns a number indicating % of completion if running Raises an exception with status else ('cancelled', 'unknown', 'failure') """ if not task: return True, '' ts = self.__getApi().getTaskInfo(task) logger.debug('Task status: %s', ts) if ts['status'] == 'running': return False, ts['progress'] if ts['status'] == 'success': return True, ts['result'] # Any other state, raises an exception raise Exception(str(ts['result'])) # Should be error message def getMachines( self, force: bool = False ) -> typing.Iterable[typing.MutableMapping[str, typing.Any]]: """ Obtains the list of machines inside XenServer. Machines starting with UDS are filtered out Args: force: If true, force to update the cache, if false, tries to first get data from cache and, if valid, return this. Returns An array of dictionaries, containing: 'name' 'id' 'cluster_id' """ for m in self.__getApi().getVMs(): if m['name'][:3] == 'UDS': continue yield m def getStorages( self, force: bool = False ) -> typing.Iterable[typing.MutableMapping[str, typing.Any]]: """ Obtains the list of storages inside XenServer. Args: force: If true, force to update the cache, if false, tries to first get data from cache and, if valid, return this. Returns An array of dictionaries, containing: 'name' 'id' 'size' 'used' """ return self.__getApi().getSRs() def getStorageInfo(self, storageId: str, force=False) -> typing.MutableMapping[str, typing.Any]: """ Obtains the storage info Args: storageId: Id of the storage to get information about it force: If true, force to update the cache, if false, tries to first get data from cache and, if valid, return this. Returns A dictionary with following values 'id' -> Storage id 'name' -> Storage name 'type' -> Storage type ('data', 'iso') 'available' -> Space available, in bytes 'used' -> Space used, in bytes # 'active' -> True or False --> This is not provided by api?? (api.storagedomains.get) """ return self.__getApi().getSRInfo(storageId) def getNetworks( self, force: bool = False ) -> typing.Iterable[typing.MutableMapping[str, typing.Any]]: return self.__getApi().getNetworks() def cloneForTemplate(self, name: str, comments: str, machineId: str, sr: str): task = self.__getApi().cloneVM(machineId, name, sr) logger.debug('Task for cloneForTemplate: %s', task) return task def convertToTemplate(self, machineId: str, shadowMultiplier: int = 4) -> None: """ Publish the machine (makes a template from it so we can create COWs) and returns the template id of the creating machine Args: name: Name of the machine (care, only ascii characters and no spaces!!!) machineId: id of the machine to be published clusterId: id of the cluster that will hold the machine storageId: id of the storage tuat will contain the publication AND linked clones displayType: type of display (for XenServer admin interface only) Returns Raises an exception if operation could not be acomplished, or returns the id of the template being created. """ self.__getApi().convertToTemplate(machineId, shadowMultiplier) def removeTemplate(self, templateId: str) -> None: """ Removes a template from XenServer server Returns nothing, and raises an Exception if it fails """ self.__getApi().removeTemplate(templateId) def startDeployFromTemplate(self, name: str, comments: str, templateId: str) -> str: """ Deploys a virtual machine on selected cluster from selected template Args: name: Name (sanitized) of the machine comments: Comments for machine templateId: Id of the template to deploy from clusterId: Id of the cluster to deploy to displayType: 'vnc' or 'spice'. Display to use ad XenServer admin interface memoryMB: Memory requested for machine, in MB guaranteedMB: Minimum memory guaranteed for this machine Returns: Id of the machine being created form template """ return self.__getApi().cloneTemplate(templateId, name) def getVMPowerState(self, machineId: str) -> str: """ Returns current machine power state """ return self.__getApi().getVMPowerState(machineId) def startVM(self, machineId: str, asnc: bool = True) -> typing.Optional[str]: """ Tries to start a machine. No check is done, it is simply requested to XenServer. This start also "resume" suspended/paused machines Args: machineId: Id of the machine Returns: """ return self.__getApi().startVM(machineId, asnc) def stopVM(self, machineId: str, asnc: bool = True) -> typing.Optional[str]: """ Tries to start a machine. No check is done, it is simply requested to XenServer Args: machineId: Id of the machine Returns: """ return self.__getApi().stopVM(machineId, asnc) def resetVM(self, machineId: str, asnc: bool = True) -> typing.Optional[str]: """ Tries to start a machine. No check is done, it is simply requested to XenServer Args: machineId: Id of the machine Returns: """ return self.__getApi().resetVM(machineId, asnc) def canSuspendVM(self, machineId: str) -> bool: """ The machine can be suspended only when "suspend" is in their operations list (mush have xentools installed) Args: machineId: Id of the machine Returns: True if the machien can be suspended """ return self.__getApi().canSuspendVM(machineId) def suspendVM(self, machineId: str, asnc: bool = True) -> typing.Optional[str]: """ Tries to start a machine. No check is done, it is simply requested to XenServer Args: machineId: Id of the machine Returns: """ return self.__getApi().suspendVM(machineId, asnc) def resumeVM(self, machineId: str, asnc: bool = True) -> typing.Optional[str]: """ Tries to start a machine. No check is done, it is simply requested to XenServer Args: machineId: Id of the machine Returns: """ return self.__getApi().resumeVM(machineId, asnc) def removeVM(self, machineId: str) -> None: """ Tries to delete a machine. No check is done, it is simply requested to XenServer Args: machineId: Id of the machine Returns: """ self.__getApi().removeVM(machineId) def configureVM(self, machineId: str, netId: str, mac: str, memory: int) -> None: self.__getApi().configureVM(machineId, mac={ 'network': netId, 'mac': mac }, memory=memory) def provisionVM(self, machineId: str, asnc: bool = True) -> str: return self.__getApi().provisionVM(machineId, asnc=asnc) def getMacRange(self) -> str: return self.macsRange.value @staticmethod def test(env: 'Environment', data: 'Module.ValuesType') -> typing.List[typing.Any]: """ Test XenServer Connectivity Args: env: environment passed for testing (temporal environment passed) data: data passed for testing (data obtained from the form definition) Returns: Array of two elements, first is True of False, depending on test (True is all right, false is error), second is an String with error, preferably internacionalizated.. """ # try: # # We instantiate the provider, but this may fail... # instance = Provider(env, data) # logger.debug('Methuselah has {0} years and is {1} :-)' # .format(instance.methAge.value, instance.methAlive.value)) # except ServiceProvider.ValidationException as e: # # If we say that meth is alive, instantiation will # return [False, str(e)] # except Exception as e: # logger.exception("Exception caugth!!!") # return [False, str(e)] # return [True, _('Nothing tested, but all went fine..')] xe = XenProvider(env, data) try: xe.testConnection() return [True, _('Connection test successful')] except Exception as e: return [False, _("Connection failed: {}").format(str(e))]
class OpenNebulaProvider(ServiceProvider): # pylint: disable=too-many-public-methods # : What kind of services we offer, this are classes inherited from Service offers = [LiveService] # : Name to show the administrator. This string will be translated BEFORE # : sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) typeName = _('OpenNebula Platform Provider') # : Type used internally to identify this provider typeType = 'openNebulaPlatform' # : Description shown at administration interface for this provider typeDescription = _('OpenNebula platform service provider') # : Icon file used as icon for this provider. This string will be translated # : BEFORE sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) iconFile = 'provider.png' # now comes the form fields # There is always two fields that are requested to the admin, that are: # Service Name, that is a name that the admin uses to name this provider # Description, that is a short description that the admin gives to this provider # Now we are going to add a few fields that we need to use this provider # Remember that these are "dummy" fields, that in fact are not required # but used for sample purposes # If we don't indicate an order, the output order of fields will be # "random" host = gui.TextField(length=64, label=_('Host'), order=1, tooltip=_('OpenNebula Host'), required=True) port = gui.NumericField( length=5, label=_('Port'), defvalue='2633', order=2, tooltip=_('OpenNebula Port (default is 2633 for non ssl connection)'), required=True) ssl = gui.CheckBoxField( label=_('Use SSL'), order=3, tooltip= _('If checked, the connection will be forced to be ssl (will not work if server is not providing ssl)' )) username = gui.TextField( length=32, label=_('Username'), order=4, tooltip=_('User with valid privileges on OpenNebula'), required=True, defvalue='oneadmin') password = gui.PasswordField( lenth=32, label=_('Password'), order=5, tooltip=_('Password of the user of OpenNebula'), required=True) maxPreparingServices = gui.NumericField( length=3, label=_('Creation concurrency'), defvalue='10', minValue=1, maxValue=65536, order=50, tooltip=_('Maximum number of concurrently creating VMs'), required=True, tab=gui.ADVANCED_TAB) maxRemovingServices = gui.NumericField( length=3, label=_('Removal concurrency'), defvalue='5', minValue=1, maxValue=65536, order=51, tooltip=_('Maximum number of concurrently removing VMs'), required=True, tab=gui.ADVANCED_TAB) timeout = gui.NumericField( length=3, label=_('Timeout'), defvalue='10', order=90, tooltip=_('Timeout in seconds of connection to OpenNebula'), required=True, tab=gui.ADVANCED_TAB) # Own variables _api: typing.Optional[on.client.OpenNebulaClient] = None def initialize(self, values: 'Module.ValuesType') -> None: ''' We will use the "autosave" feature for form fields ''' # Just reset _api connection variable self._api = None if values: self.timeout.value = validators.validateTimeout(self.timeout.value) logger.debug('Endpoint: %s', self.endpoint) @property def endpoint(self) -> str: return 'http{}://{}:{}/RPC2'.format('s' if self.ssl.isTrue() else '', self.host.value, self.port.value) @property def api(self) -> on.client.OpenNebulaClient: if self._api is None: self._api = on.client.OpenNebulaClient(self.username.value, self.password.value, self.endpoint) return self._api def resetApi(self) -> None: self._api = None def sanitizeVmName(self, name: str) -> str: return on.sanitizeName(name) def testConnection(self) -> typing.List[typing.Any]: ''' Test that conection to OpenNebula server is fine Returns True if all went fine, false if id didn't ''' try: if self.api.version[0] < '4': return [ False, 'OpenNebula version is not supported (required version 4.1 or newer)' ] except Exception as e: return [False, '{}'.format(e)] return [True, _('Opennebula test connection passed')] def getDatastores( self, datastoreType: int = 0) -> typing.Iterable[on.types.StorageType]: yield from on.storage.enumerateDatastores(self.api, datastoreType) def getTemplates( self, force: bool = False) -> typing.Iterable[on.types.TemplateType]: yield from on.template.getTemplates(self.api, force) def makeTemplate(self, fromTemplateId: str, name, toDataStore: str) -> str: return on.template.create(self.api, fromTemplateId, name, toDataStore) def checkTemplatePublished(self, templateId: str) -> bool: return on.template.checkPublished(self.api, templateId) def removeTemplate(self, templateId: str) -> None: on.template.remove(self.api, templateId) def deployFromTemplate(self, name: str, templateId: str) -> str: return on.template.deployFrom(self.api, templateId, name) def getMachineState(self, machineId: str) -> on.types.VmState: ''' Returns the state of the machine This method do not uses cache at all (it always tries to get machine state from OpenNebula server) Args: machineId: Id of the machine to get state Returns: one of the on.VmState Values ''' return on.vm.getMachineState(self.api, machineId) def getMachineSubstate(self, machineId: str) -> int: ''' Returns the LCM_STATE of a machine (STATE must be ready or this will return -1) ''' return on.vm.getMachineSubstate(self.api, machineId) def startMachine(self, machineId: str) -> None: ''' Tries to start a machine. No check is done, it is simply requested to OpenNebula. This start also "resume" suspended/paused machines Args: machineId: Id of the machine Returns: ''' on.vm.startMachine(self.api, machineId) def stopMachine(self, machineId: str) -> None: ''' Tries to stop a machine. No check is done, it is simply requested to OpenNebula Args: machineId: Id of the machine Returns: ''' on.vm.stopMachine(self.api, machineId) def suspendMachine(self, machineId: str) -> None: ''' Tries to suspend a machine. No check is done, it is simply requested to OpenNebula Args: machineId: Id of the machine Returns: ''' on.vm.suspendMachine(self.api, machineId) def shutdownMachine(self, machineId: str) -> None: ''' Tries to shutdown "gracefully" a machine. No check is done, it is simply requested to OpenNebula Args: machineId: Id of the machine Returns: ''' on.vm.shutdownMachine(self.api, machineId) def resetMachine(self, machineId: str) -> None: ''' Resets a machine (hard-reboot) ''' on.vm.resetMachine(self.api, machineId) def removeMachine(self, machineId: str) -> None: ''' Tries to delete a machine. No check is done, it is simply requested to OpenNebula Args: machineId: Id of the machine Returns: ''' on.vm.removeMachine(self.api, machineId) def getNetInfo( self, machineId: str, networkId: typing.Optional[str] = None) -> typing.Tuple[str, str]: ''' Changes the mac address of first nic of the machine to the one specified ''' return on.vm.getNetInfo(self.api, machineId, networkId) def getConsoleConnection(self, machineId: str) -> typing.Dict[str, typing.Any]: display = on.vm.getDisplayConnection(self.api, machineId) if display is None: raise Exception('Invalid console connection on OpenNebula!!!') return { 'type': display['type'], 'address': display['host'], 'port': display['port'], 'secure_port': -1, 'monitors': 1, 'cert_subject': '', 'ticket': { 'value': display['passwd'], 'expiry': '' } } def desktopLogin(self, machineId: str, username: str, password: str, domain: str): ''' Not provided by OpenNebula API right now ''' return @staticmethod def test(env: 'Environment', data: 'Module.ValuesType') -> typing.List[typing.Any]: return OpenNebulaProvider(env, data).testConnection()
class TSNXTransport(BaseNXTransport): """ Provides access via NX to service. This transport can use an domain. If username processed by authenticator contains '@', it will split it and left-@-part will be username, and right password """ typeName = _('NX v3.5 (DEPRECATED)') typeType = 'TSNXTransport' typeDescription = _('NX protocol v3.5. Tunneled connection.') iconFile = 'nx.png' protocol = transports.protocols.NX group = transports.TUNNELED_GROUP tunnelServer = gui.TextField( label=_('Tunnel server'), order=1, tooltip= _('IP or Hostname of tunnel server sent to client device ("public" ip) and port. (use HOST:PORT format)' ), tab=gui.TUNNEL_TAB) useEmptyCreds = gui.CheckBoxField( label=_('Empty creds'), order=3, tooltip=_('If checked, the credentials used to connect will be emtpy'), tab=gui.CREDENTIALS_TAB) fixedName = gui.TextField( label=_('Username'), order=4, tooltip=_( 'If not empty, this username will be always used as credential'), tab=gui.CREDENTIALS_TAB) fixedPassword = gui.PasswordField( label=_('Password'), order=5, tooltip=_( 'If not empty, this password will be always used as credential'), tab=gui.CREDENTIALS_TAB) listenPort = gui.NumericField( label=_('Listening port'), length=5, order=6, tooltip=_('Listening port of NX (ssh) at client machine'), defvalue='22') connection = gui.ChoiceField( label=_('Connection'), order=7, tooltip=_('Connection speed for this transport (quality)'), values=[ { 'id': 'modem', 'text': 'modem' }, { 'id': 'isdn', 'text': 'isdn' }, { 'id': 'adsl', 'text': 'adsl' }, { 'id': 'wan', 'text': 'wan' }, { 'id': 'lan', 'text': 'lan' }, ], tab=gui.PARAMETERS_TAB) session = gui.ChoiceField(label=_('Session'), order=8, tooltip=_('Desktop session'), values=[ { 'id': 'gnome', 'text': 'gnome' }, { 'id': 'kde', 'text': 'kde' }, { 'id': 'cde', 'text': 'cde' }, ], tab=gui.PARAMETERS_TAB) cacheDisk = gui.ChoiceField(label=_('Disk Cache'), order=9, tooltip=_('Cache size en Mb stored at disk'), values=[ { 'id': '0', 'text': '0 Mb' }, { 'id': '32', 'text': '32 Mb' }, { 'id': '64', 'text': '64 Mb' }, { 'id': '128', 'text': '128 Mb' }, { 'id': '256', 'text': '256 Mb' }, { 'id': '512', 'text': '512 Mb' }, ], tab=gui.PARAMETERS_TAB) cacheMem = gui.ChoiceField(label=_('Memory Cache'), order=10, tooltip=_('Cache size en Mb kept at memory'), values=[ { 'id': '4', 'text': '4 Mb' }, { 'id': '8', 'text': '8 Mb' }, { 'id': '16', 'text': '16 Mb' }, { 'id': '32', 'text': '32 Mb' }, { 'id': '64', 'text': '64 Mb' }, { 'id': '128', 'text': '128 Mb' }, ], tab=gui.PARAMETERS_TAB) screenSize = gui.ChoiceField(label=_('Screen size'), order=10, tooltip=_('Screen size'), defvalue=CommonPrefs.SZ_FULLSCREEN, values=[{ 'id': CommonPrefs.SZ_640x480, 'text': '640x480' }, { 'id': CommonPrefs.SZ_800x600, 'text': '800x600' }, { 'id': CommonPrefs.SZ_1024x768, 'text': '1024x768' }, { 'id': CommonPrefs.SZ_1366x768, 'text': '1366x768' }, { 'id': CommonPrefs.SZ_1920x1080, 'text': '1920x1080' }, { 'id': CommonPrefs.SZ_FULLSCREEN, 'text': ugettext_lazy('Full Screen') }], tab=gui.PARAMETERS_TAB) _tunnelServer: str = '' _tunnelCheckServer: str = '' _useEmptyCreds: bool = False _fixedName: str = '' _fixedPassword: str = '' _listenPort: str = '' _connection: str = '' _session: str = '' _cacheDisk: str = '' _cacheMem: str = '' _screenSize: str = '' def initialize(self, values: 'Module.ValuesType'): if values: if values['tunnelServer'].find(':') == -1: raise transports.Transport.ValidationException( _('Must use HOST:PORT in Tunnel Server Field')) self._tunnelServer = values['tunnelServer'] self._tunnelCheckServer = '' self._useEmptyCreds = gui.strToBool(values['useEmptyCreds']) self._fixedName = values['fixedName'] self._fixedPassword = values['fixedPassword'] self._listenPort = values['listenPort'] self._connection = values['connection'] self._session = values['session'] self._cacheDisk = values['cacheDisk'] self._cacheMem = values['cacheMem'] self._screenSize = values['screenSize'] def marshal(self) -> bytes: """ Serializes the transport data so we can store it in database """ return str.join('\t', [ 'v2', gui.boolToStr(self._useEmptyCreds), self._fixedName, self._fixedPassword, self._listenPort, self._connection, self._session, self._cacheDisk, self._cacheMem, self._tunnelServer, self._tunnelCheckServer, self._screenSize ]).encode('utf8') def unmarshal(self, data: bytes) -> None: values = data.decode('utf8').split('\t') if values[0] in ('v1', 'v2'): self._useEmptyCreds = gui.strToBool(values[1]) (self._fixedName, self._fixedPassword, self._listenPort, self._connection, self._session, self._cacheDisk, self._cacheMem, self._tunnelServer, self._tunnelCheckServer) = values[2:11] self._screenSize = values[11] if values[ 0] == 'v2' else CommonPrefs.SZ_FULLSCREEN def valuesDict(self) -> gui.ValuesDictType: return { 'useEmptyCreds': gui.boolToStr(self._useEmptyCreds), 'fixedName': self._fixedName, 'fixedPassword': self._fixedPassword, 'listenPort': self._listenPort, 'connection': self._connection, 'session': self._session, 'cacheDisk': self._cacheDisk, 'cacheMem': self._cacheMem, 'tunnelServer': self._tunnelServer } def getUDSTransportScript( # pylint: disable=too-many-locals self, userService: 'models.UserService', transport: 'models.Transport', ip: str, os: typing.Dict[str, str], user: '******', password: str, request: 'HttpRequest' ) -> typing.Tuple[str, str, typing.Dict[str, typing.Any]]: prefs = self.screenSize.value username = user.getUsernameForAuth() proc = username.split('@') username = proc[0] if self._fixedName is not '': username = self._fixedName if self._fixedPassword is not '': password = self._fixedPassword if self._useEmptyCreds is True: usernamsizerd = '', '' tunpass = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _i in range(12)) tunuser = TicketStore.create(tunpass) sshServer = self._tunnelServer if ':' not in sshServer: sshServer += ':443' sshHost, sshPort = sshServer.split(':') logger.debug('Username generated: %s, password: %s', tunuser, tunpass) width, height = CommonPrefs.getWidthHeight(prefs) # Fix username/password acording to os manager username, password = userService.processUserPassword( username, password) r = NXFile(username=username, password=password, width=width, height=height) r.host = '{address}' r.port = '{port}' r.linkSpeed = self._connection r.desktop = self._session r.cachedisk = self._cacheDisk r.cachemem = self._cacheMem osName = { OsDetector.Windows: 'windows', OsDetector.Linux: 'linux', OsDetector.Macintosh: 'macosx' }.get(os['OS']) if osName is None: return super().getUDSTransportScript(userService, transport, ip, os, user, password, request) sp = { 'ip': ip, 'tunUser': tunuser, 'tunPass': tunpass, 'tunHost': sshHost, 'tunPort': sshPort, 'port': self._listenPort, 'as_file_for_format': r.as_file_for_format } return self.getScript('scripts/{}/direct.py', osName, sp)
class ProxmoxLinkedService(Service): # pylint: disable=too-many-public-methods """ Proxmox Linked clones service. This is based on creating a template from selected vm, and then use it to """ # : Name to show the administrator. This string will be translated BEFORE # : sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) typeName = _('Proxmox Linked Clone') # : Type used internally to identify this provider typeType = 'ProxmoxLinkedService' # : Description shown at administration interface for this provider typeDescription = _('Proxmox Services based on templates and COW (experimental)') # : Icon file used as icon for this provider. This string will be translated # : BEFORE sending it to administration interface, so don't forget to # : mark it as _ (using ugettext_noop) iconFile = 'service.png' # Functional related data # : If the service provides more than 1 "deployed user" (-1 = no limit, # : 0 = ???? (do not use it!!!), N = max number to deploy maxDeployed = -1 # : If we need to generate "cache" for this service, so users can access the # : provided services faster. Is usesCache is True, you will need also # : set publicationType, do take care about that! usesCache = True # : Tooltip shown to user when this item is pointed at admin interface, none # : because we don't use it cacheTooltip = _('Number of desired machines to keep running waiting for a user') # : If we need to generate a "Level 2" cache for this service (i.e., L1 # : could be running machines and L2 suspended machines) usesCache_L2 = True # : Tooltip shown to user when this item is pointed at admin interface, None # : also because we don't use it cacheTooltip_L2 = _('Number of desired VMs to keep stopped waiting for use') # : If the service needs a s.o. manager (managers are related to agents # : provided by services itselfs, i.e. virtual machines with actors) needsManager = True # : If true, the system can't do an automatic assignation of a deployed user # : service from this service mustAssignManually = False canReset = True # : Types of publications (preparated data for deploys) # : In our case, we do no need a publication, so this is None publicationType = ProxmoxPublication # : Types of deploys (services in cache and/or assigned to users) deployedType = ProxmoxDeployment allowedProtocols = protocols.GENERIC # + (protocols.SPICE,) servicesTypeProvided = (serviceTypes.VDI,) pool = gui.ChoiceField( label=_("Pool"), order=1, tooltip=_('Pool that will contain UDS created vms'), # tab=_('Machine'), # required=True, defvalue='' ) ha = gui.ChoiceField( label=_('HA'), order=2, tooltip=_('Select if HA is enabled and HA group for machines of this service'), rdonly=True ) machine = gui.ChoiceField( label=_("Base Machine"), order=110, fills={ 'callbackName': 'pmFillResourcesFromMachine', 'function': helpers.getStorage, 'parameters': ['machine', 'ov', 'ev'] }, tooltip=_('Service base machine'), tab=_('Machine'), required=True ) datastore = gui.ChoiceField( label=_("Storage"), rdonly=False, order=111, tooltip=_('Storage for publications & machines.'), tab=_('Machine'), required=True ) baseName = gui.TextField( label=_('Machine Names'), rdonly=False, order=115, tooltip=_('Base name for clones from this machine'), tab=_('Machine'), required=True ) lenName = gui.NumericField( length=1, label=_('Name Length'), defvalue=5, order=116, tooltip=_('Size of numeric part for the names of these machines'), tab=_('Machine'), required=True ) ov = gui.HiddenField(value=None) ev = gui.HiddenField(value=None) # We need to keep the env so we can instantiate the Provider def initialize(self, values: 'Module.ValuesType') -> None: if values: self.baseName.value = validators.validateHostname(self.baseName.value, 15, asPattern=True) # if int(self.memory.value) < 128: # raise Service.ValidationException(_('The minimum allowed memory is 128 Mb')) def initGui(self) -> None: # Here we have to use "default values", cause values aren't used at form initialization # This is that value is always '', so if we want to change something, we have to do it # at defValue self.ov.defValue = self.parent().serialize() self.ev.defValue = self.parent().env.key # This is not the same case, values is not the "value" of the field, but # the list of values shown because this is a "ChoiceField" self.machine.setValues([gui.choiceItem(str(m.vmid), '{}\{}'.format(m.node, m.name or m.vmid)) for m in self.parent().listMachines() if m.name and m.name[:3] != 'UDS']) self.pool.setValues([gui.choiceItem('', _('None'))] + [gui.choiceItem(p.poolid, p.poolid) for p in self.parent().listPools()]) self.ha.setValues( [ gui.choiceItem('', _('Enabled')), gui.choiceItem('__', _('Disabled')) ] + [ gui.choiceItem(group, group) for group in self.parent().listHaGroups() ] ) def parent(self) -> 'ProxmoxProvider': return typing.cast('ProxmoxProvider', super().parent()) def sanitizeVmName(self, name: str) -> str: """ Proxmox only allows machine names with [a-zA-Z0-9_-] """ return re.sub("[^a-zA-Z0-9_-]", "-", name) def makeTemplate(self, vmId: int) -> None: self.parent().makeTemplate(vmId) def cloneMachine(self, name: str, description: str, vmId: int = -1) -> 'client.types.VmCreationResult': name = self.sanitizeVmName(name) pool = self.pool.value or None if vmId == -1: # vmId == -1 if cloning for template return self.parent().cloneMachine( self.machine.value, name, description, linkedClone=False, toStorage=self.datastore.value, toPool=pool ) return self.parent().cloneMachine( vmId, name, description, linkedClone=True, toStorage=self.datastore.value, toPool=pool ) def getMachineInfo(self, vmId: int) -> 'client.types.VMInfo': return self.parent().getMachineInfo(vmId) def getMac(self, vmId: int) -> str: config = self.parent().getMachineConfiguration(vmId) return config.networks[0].mac def getTaskInfo(self, node: str, upid: str) -> 'client.types.TaskStatus': return self.parent().getTaskInfo(node, upid) def startMachine(self,vmId: int) -> 'client.types.UPID': return self.parent().startMachine(vmId) def stopMachine(self, vmId: int) -> 'client.types.UPID': return self.parent().stopMachine(vmId) def suspendMachine(self, vmId: int) -> 'client.types.UPID': return self.parent().suspendMachine(vmId) def shutdownMachine(self, vmId: int) -> 'client.types.UPID': return self.parent().shutdownMachine(vmId) def removeMachine(self, vmId: int) -> 'client.types.UPID': # First, remove from HA if needed self.disableHA(vmId) # And remove it return self.parent().removeMachine(vmId) def enableHA(self, vmId: int, started: bool = False) -> None: if self.ha.value == '__': return self.parent().enableHA(vmId, started, self.ha.value or None) def disableHA(self, vmId: int) -> None: if self.ha.value == '__': return self.parent().disableHA(vmId) def setProtection(self, vmId: int, node: typing.Optional[str] = None, protection: bool=False) -> None: self.parent().setProtection(vmId, node, protection) def getBaseName(self) -> str: return self.baseName.value def getLenName(self) -> int: return int(self.lenName.value) def isHaEnabled(self) -> bool: return self.ha.isTrue() def getConsoleConnection(self, machineId: str) -> typing.Optional[typing.MutableMapping[str, typing.Any]]: return self.parent().getConsoleConnection(machineId)
class TX2GOTransport(BaseX2GOTransport): """ Provides access via X2GO to service. This transport can use an domain. If username processed by authenticator contains '@', it will split it and left-@-part will be username, and right password """ typeName = _('X2Go') typeType = 'TX2GOTransport' typeDescription = _('X2Go access (Experimental). Tunneled connection.') group = transports.TUNNELED_GROUP tunnelServer = gui.TextField( label=_('Tunnel server'), order=1, tooltip= _('IP or Hostname of tunnel server sent to client device ("public" ip) and port. (use HOST:PORT format)' ), tab=gui.TUNNEL_TAB) fixedName = BaseX2GOTransport.fixedName screenSize = BaseX2GOTransport.screenSize desktopType = BaseX2GOTransport.desktopType customCmd = BaseX2GOTransport.customCmd sound = BaseX2GOTransport.sound exports = BaseX2GOTransport.exports speed = BaseX2GOTransport.speed soundType = BaseX2GOTransport.soundType keyboardLayout = BaseX2GOTransport.keyboardLayout pack = BaseX2GOTransport.pack quality = BaseX2GOTransport.quality def initialize(self, values: 'Module.ValuesType'): if values: if values['tunnelServer'].count(':') != 1: raise BaseX2GOTransport.ValidationException( _('Must use HOST:PORT in Tunnel Server Field')) def getUDSTransportScript( # pylint: disable=too-many-locals self, userService: 'models.UserService', transport: 'models.Transport', ip: str, os: typing.Dict[str, str], user: '******', password: str, request: 'HttpRequest' ) -> typing.Tuple[str, str, typing.Dict[str, typing.Any]]: ci = self.getConnectionInfo(userService, user, password) username = ci['username'] priv, pub = self.getAndPushKey(username, userService) width, height = self.getScreenSize() rootless = False desktop = self.desktopType.value if desktop == "UDSVAPP": desktop = "/usr/bin/udsvapp " + self.customCmd.value rootless = True xf = x2go_file.getTemplate(speed=self.speed.value, pack=self.pack.value, quality=self.quality.value, sound=self.sound.isTrue(), soundSystem=self.sound.value, windowManager=desktop, exports=self.exports.isTrue(), rootless=rootless, width=width, height=height, user=username) tunpass = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _i in range(12)) tunuser = TicketStore.create(tunpass) sshHost, sshPort = self.tunnelServer.value.split(':') # data data = { 'os': os['OS'], 'ip': ip, 'port': 22, 'key': priv, 'width': width, 'height': height, 'printers': True, 'drives': self.exports.isTrue(), 'fullScreen': width == -1 or height == -1, 'this_server': request.build_absolute_uri('/'), 'xf': xf } m = tools.DictAsObj(data) osName = { OsDetector.Windows: 'windows', OsDetector.Linux: 'linux', # OsDetector.Macintosh: 'macosx' }.get(os['OS']) if osName is None: return super().getUDSTransportScript(userService, transport, ip, os, user, password, request) sp = { 'tunUser': tunuser, 'tunPass': tunpass, 'tunHost': sshHost, 'tunPort': sshPort, 'ip': ip, 'port': '22', 'key': priv, 'xf': xf } return self.getScript('scripts/{}/direct.py', osName, sp)