def __cli_check(self, command, timeout=15.0): """ Execute cleep-cli check specified by command Args: command (string): cli command to execute Returns: dict: command output """ console = Console() res = console.command(command, timeout) self.logger.debug('Cli command "%s" output: %s | %s' % (command, res['stdout'], res['stderr'])) if res['returncode'] != 0: self.logger.exception('Command "%s"', command) raise CommandError('Command failed') try: return json.loads(''.join(res['stdout'])) except Exception as error: self.logger.exception('Error parsing command "%s" output' % command) raise CommandError( 'Error parsing check result. Check Cleep logs') from error
def build_application(self, module_name): """ Build application archive (zip format) Archive is not protected by password Args: module_name (string): module name Raises: Exception: if build failed """ cmd = self.CLI_BUILD_APP_CMD % (self.CLI, module_name) self.logger.debug('Build app cmd: %s' % cmd) console = Console() res = console.command(cmd, 60.0) self.logger.info('Build app result: %s | %s' % (res['stdout'], res['stderr'])) if res['returncode'] != 0: raise CommandError('Error building application. Check Cleep logs.') try: self.__last_application_build = json.loads(res['stdout'][0]) except Exception as error: self.logger.exception( 'Error parsing app build command "%s" output' % cmd) raise CommandError( 'Error building application. Check Cleep logs.') from error
def download_documentation(self, module_name): """ Download documentation (html as archive tar.gz) Args: module_name (string): module name Returns: dict: archive infos:: { data: filepath filename: new filename } Raises: CommandError: command failed error """ self.logger.info('Download documentation html archive') cmd = self.CLI_DOCS_ZIP_PATH_CMD % (self.CLI, module_name) self.logger.debug('Doc zip path cmd: %s' % cmd) console = Console() res = console.command(cmd) if res['returncode'] != 0: raise CommandError(''.join(res['stdout'])) zip_path = res['stdout'][0].split('=')[1] self.logger.debug('Module "%s" docs path "%s"' % (module_name, zip_path)) return {'filepath': zip_path, 'filename': os.path.basename(zip_path)}
def tweak_power_led(self, enable): """ Tweak raspberry pi power led Note: Infos from https://www.jeffgeerling.com/blogs/jeff-geerling/controlling-pwr-act-leds-raspberry-pi Args: enable (bool): True to turn on led """ if not os.path.exists('/sys/class/leds/led1'): self.logger.info('Power led not found on this device') return raspi = Tools.raspberry_pi_infos() off_value = '0' if raspi['model'].lower().find('zero') else '1' on_value = '1' if raspi['model'].lower().find('zero') else '0' echo_value = on_value if enable else off_value console = Console() resp = console.command('echo %s > /sys/class/leds/led1/brightness' % echo_value) if resp['returncode'] != 0: raise CommandError('Error tweaking power led') # store led status self._set_config_field('enablepowerled', enable)
def download_logs(self): """ Download logs file Returns: string: script full path Raises: Exception: if error occured """ if not os.path.exists(self.log_file): # file doesn't exist, raise exception raise CommandError('Logs file doesn\'t exist') # log file exists, zip it file_descriptor = NamedTemporaryFile(delete=False) log_filename = file_descriptor.name self.logger.debug('Zipped log filename: %s' % log_filename) archive = ZipFile(file_descriptor, 'w', ZIP_DEFLATED) archive.write(self.log_file, 'cleep.log') archive.close() now = datetime.now() filename = 'cleep_%d%02d%02d_%02d%02d%02d.zip' % ( now.year, now.month, now.day, now.hour, now.minute, now.second) return {'filepath': log_filename, 'filename': filename}
def save_wired_dhcp_configuration(self, interface_name): """ Save wired dhcp configuration Args: interface_name (string): interface name """ # check params self._check_parameters([ {'name': 'interface_name', 'value': interface_name, 'type': str}, ]) if self.dhcpcd.is_installed(): # save config using dhcpcd # delete configuration for specified interface (unconfigured interface in dhcpcd is considered as DHCP) self.dhcpcd.delete_interface(interface_name) else: # save config using /etc/network/interface file # delete existing configuration for specified interface self.etcnetworkinterfaces.delete_interface(interface_name) # finally add new configuration if not self.etcnetworkinterfaces.add_dhcp_interface( interface_name, EtcNetworkInterfaces.OPTION_AUTO + EtcNetworkInterfaces.OPTION_HOTPLUG): self.logger.error( 'Unable to save wired dhcp configuration (interfaces): unable to add interface %s' % interface_name ) raise CommandError('Unable to save configuration') # restart interface self.reconfigure_wired_interface(interface_name)
def save_wifi_network_configuration(self, interface_name, network_name, encryption, password=None, hidden=False): """ Save wifi network configuration Args: interface_name (string): interface name network_name (string): network to connect interface to encryption (string): encryption type (see WpaSupplicantConf.ENCRYPTION_TYPE_XXX) password (string): network connection password hidden (bool): True if network is hidden Returns: bool: True if connection succeed Raises: CommandError if network adding failed """ # check params self._check_parameters([ {'name': 'interface_name', 'value': interface_name, 'type': str}, {'name': 'network_name', 'value': network_name, 'type': str}, { 'name': 'encryption', 'value': encryption, 'type': str, 'validator': lambda val: val in WpaSupplicantConf.ENCRYPTION_TYPES, }, ]) # save config in wpa_supplicant.conf file if not self.wpasupplicantconf.add_network(network_name, encryption, password, hidden, interface=interface_name): raise CommandError('Unable to save network configuration') # reconfigure interface return self.wpacli.reconfigure_interface(interface_name)
def create_application(self, module_name): """ Create new application skel Args: module_name (string): module name Raises: CommandError: if command failed """ self.__stop_watcher() cmd = self.CLI_NEW_APPLICATION_CMD % (self.CLI, module_name) self.logger.debug('Create app cmd: %s' % cmd) try: console = Console() res = console.command(cmd, 10.0) self.logger.info('Create app cmd result: %s %s' % (res['stdout'], res['stderr'])) if res['returncode'] != 0: raise CommandError( 'Error during application creation. Check Cleep logs.') # sync new app content console.command(self.CLI_SYNC_MODULE_CMD % module_name) finally: self.__start_watcher()
def delete_wifi_network_configuration(self, interface_name, network_name): """ Delete specified network configuration Args: interface_name (string): interface name network_name (string): network config to delete Returns: bool: True if network deleted Raises: CommandError if network deletion failed """ # check params self._check_parameters([ {'name': 'interface_name', 'value': interface_name, 'type': str}, {'name': 'network_name', 'value': network_name, 'type': str}, ]) if not self.wpasupplicantconf.delete_network(network_name, interface=interface_name): raise CommandError('Unable to delete network configuration') # reconfigure interface return self.wpacli.reconfigure_interface(interface_name)
def update_wifi_network_password(self, interface_name, network_name, password): """ Update wifi network configuration Args: interface (string): interface name network (string): network to connect interface to password (string): network connection password Returns: bool: True if update succeed Raises: CommandError """ # check params self._check_parameters([ {'name': 'interface_name', 'value': interface_name, 'type': str}, {'name': 'network_name', 'value': network_name, 'type': str}, {'name': 'password', 'value': password, 'type': str}, ]) if not self.wpasupplicantconf.update_network_password(network_name, password, interface=interface_name): raise CommandError('Unable to update network password') # reconfigure interface return self.wpacli.reconfigure_interface(interface_name)
def get_onewire_devices(self): """ Scan for devices connected on 1wire bus Returns: dict: list of onewire devices:: { device (dict): onewire device path (string): device onewire path } """ onewires = [] if not self.onewire_driver.is_installed(): raise CommandError("Onewire driver is not installed") devices = glob.glob(os.path.join(self.ONEWIRE_PATH, "28*")) self.logger.debug("Onewire devices: %s" % devices) for device in devices: onewires.append({ "device": os.path.basename(device), "path": os.path.join(device, self.ONEWIRE_SLAVE), }) return onewires
def disable_wifi_network(self, interface_name, network_name): """ Disable wifi network Args: interface_name (string): interface name network_name (string): network name Returns: bool: True if network updated Raises: CommandError """ # check params self._check_parameters([ {'name': 'interface_name', 'value': interface_name, 'type': str}, {'name': 'network_name', 'value': network_name, 'type': str}, ]) if not self.wpasupplicantconf.disable_network(network_name, interface=interface_name): raise CommandError('Unable to disable network') # reconfigure interface return self.wpacli.reconfigure_interface(interface_name)
def _resource_acquired(self, resource_name): """ Function called when resource is acquired Args: resource_name (string): acquired resource name Raises: CommandError: if command failed """ self.logger.debug('Resource "%s" acquired', resource_name) if resource_name == "audio.playback": # play test sample if not self.alsa.play_sound(self.TEST_SOUND): raise CommandError("Unable to play test sound: internal error") # release resource self._release_resource("audio.playback") elif resource_name == "audio.capture": # record sound sound = self.alsa.record_sound(timeout=5.0) self.logger.debug("Recorded sound: %s", sound) if not self.alsa.play_sound(sound, timeout=6.0): raise CommandError("Unable to play recorded sound: internal error") # release resource self._release_resource("audio.capture") # purge file time.sleep(0.5) try: os.remove(sound) except Exception: self.logger.warning('Unable to delete recorded test sound "%s"', sound) else: self.logger.error('Unsupported resource "%s" acquired', resource_name)
def turn_off(self, device_uuid): """ Turn off specified device Args: device_uuid (string): device identifier Returns: bool: True if command executed successfully Raises: CommandError """ device = self._get_device(device_uuid) if device is None: raise CommandError("Device not found") if device["mode"] != self.MODE_OUTPUT: raise CommandError( 'Gpio "%s" configured as "%s" cannot be turned off' % (device["gpio"], device["mode"]) ) # turn off relay self.logger.debug("Turn off GPIO %s" % device["gpio"]) self._gpio_output(device["pin"], GPIO_HIGH) # save current state device["on"] = False if device["keep"]: self._update_device(device_uuid, device) # broadcast event self.gpios_gpio_off.send( params={"gpio": device["gpio"], "init": False, "duration": 0}, device_id=device_uuid, ) return True
def update_gpio(self, device_uuid, name, keep, inverted, command_sender): """ Update gpio Args: device_uuid (string): device identifier name (string): gpio name keep (bool): keep status flag inverted (bool): inverted flag command_sender (string): command sender Returns: dict: updated gpio device Raises: CommandError MissingParameter Unauthorized InvalidParameter """ # fix command_sender: rpcserver is the default gpio entry point if command_sender == "rpcserver": command_sender = "gpios" # check values self._check_parameters( [ {"name": "device_uuid", "value": device_uuid, "type": str}, {"name": "name", "value": name, "type": str}, {"name": "keep", "value": keep, "type": bool}, {"name": "inverted", "value": inverted, "type": bool}, ] ) device = self._get_device(device_uuid) if device is None: raise InvalidParameter('Device "%s" does not exist' % device_uuid) if device["owner"] != command_sender: raise Unauthorized("Device can only be updated by its owner") # device is valid, update entry device["name"] = name device["keep"] = keep device["inverted"] = inverted if not self._update_device(device_uuid, device): raise CommandError('Failed to update device "%s"' % device["uuid"]) # relaunch watcher self._reconfigure_gpio(device) return device
def set_event_renderable(self, renderer_name, event_name, renderable): """ Set event renderable status Args: renderer_name (string): renderer name event_name (string): event name renderable (bool): True to allow event rendering for renderer, False otherwise Returns: list: list of events not renderable """ self._check_parameters([ { 'name': 'renderer_name', 'type': str, 'value': renderer_name }, { 'name': 'event_name', 'type': str, 'value': event_name }, { 'name': 'renderable', 'type': bool, 'value': renderable }, ]) # update config events_not_renderable = self._get_config_field('eventsnotrenderable') key = '%s%s%s' % (renderer_name, self.EVENT_SEPARATOR, event_name) if key in events_not_renderable and renderable: # enable event rendering events_not_renderable.remove(key) elif key not in events_not_renderable and not renderable: # disable event rendering events_not_renderable.append(key) if not self._set_config_field('eventsnotrenderable', events_not_renderable): raise CommandError('Unable to save configuration') # set event renderable status self.events_broker.set_event_renderable(event_name, renderer_name, renderable) return self.get_not_renderable_events()
def is_on(self, device_uuid): """ Return gpio status (on or off) Args: device_uuid (string): device identifier Returns: bool: True if device is on, False if device is off Raises: CommandError """ # check values device = self._get_device(device_uuid) if device is None: raise CommandError("Device not found") if device["mode"] == self.MODE_RESERVED: raise CommandError( 'Gpio "%s" configured as "%s" cannot be checked' % (device["gpio"], device["mode"]) ) return device["on"]
def get_last_coverage_report(self, module_name): """ Return last coverage report Args: module_name (string): module name """ if self.__tests_task: raise CommandError('Tests are running. Please wait end of it') cmd = self.CLI_TESTS_COV_CMD % (self.CLI, module_name) self.logger.debug('Test cov cmd: %s' % cmd) self.__tests_task = EndlessConsole(cmd, self.__tests_callback, self.__tests_end_callback) self.__tests_task.start()
def set_monitoring(self, monitoring): """ Set monitoring flag Args: monitoring (bool): monitoring flag """ self._check_parameters([ { 'name': 'monitoring', 'type': bool, 'value': monitoring }, ]) if not self._set_config_field('monitoring', monitoring): raise CommandError('Unable to save configuration')
def launch_tests(self, module_name): """ Launch unit tests Args: module_name (string): module name """ if self.__tests_task: raise CommandError('Tests are already running') cmd = self.CLI_TESTS_CMD % (self.CLI, module_name) self.logger.debug('Test cmd: %s' % cmd) self.__tests_task = EndlessConsole(cmd, self.__tests_callback, self.__tests_end_callback) self.__tests_task.start() self.tests_output_event.send( params={'messages': 'Tests execution started. Please wait...'}, to='rpc', render=False)
def set_country(self): """ Compute country (and associated alpha) from current internal position Warning: This function can take some time to find country info on slow device like raspi 1st generation (~15secs) """ # get position position = self._get_config_field("position") if not position["latitude"] and not position["longitude"]: self.logger.debug( "Unable to set country from unspecified position (%s)" % position) return # get country from position country = {"country": None, "alpha2": None} try: # search country coordinates = ((position["latitude"], position["longitude"]), ) # need a tuple geo = reverse_geocode.search(coordinates) self.logger.debug("Found country infos from position %s: %s" % (position, geo)) if (geo and len(geo) > 0 and "country_code" in geo[0] and "country" in geo[0]): country["alpha2"] = geo[0]["country_code"] country["country"] = geo[0]["country"] # save new country if not self._set_config_field("country", country): raise CommandError("Unable to save country") # send event self.country_update_event.send(params=country) except CommandError: raise except Exception: self.logger.exception("Unable to find country for position %s:" % position)
def delete_gpio(self, device_uuid, command_sender): """ Delete gpio Args: uuid: device identifier command_sender (string): command sender Returns: bool: True if device was deleted, False otherwise Raises: CommandError MissingParameter Unauthorized InvalidParameter """ # fix command_sender: rpcserver is the default gpio entry point if command_sender == "rpcserver": command_sender = "gpios" # check values self._check_parameters( [ {"name": "device_uuid", "value": device_uuid, "type": str}, ] ) device = self._get_device(device_uuid) if device is None: raise InvalidParameter('Device "%s" does not exist' % device_uuid) if device["owner"] != command_sender: raise Unauthorized("Device can only be deleted by its owner") # device is valid, remove entry if not self._delete_device(device_uuid): raise CommandError('Failed to delete device "%s"' % device["uuid"]) self._deconfigure_gpio(device) return True
def download_application(self): """ Download latest generated application package Returns: dict: archive infos:: { filepath (string): filepath filename (string): filename } """ self.logger.debug('Download application archive') if not self.__last_application_build: raise CommandError('Please build application first') return { 'filepath': self.__last_application_build['package'], 'filename': os.path.basename(self.__last_application_build['package']), }
def set_crash_report(self, enable): """ Enable or disable crash report Args: enable (bool): True to enable crash report Raises: CommandError if error occured """ self._check_parameters([{ 'name': 'enable', 'type': bool, 'value': enable }]) # save config if not self._set_config_field('crashreport', enable): raise CommandError('Unable to save crash report value') # configure crash report self._configure_crash_report(enable)
def set_module_debug(self, module_name, debug): """ Set module debug flag Args: module_name (string): module name debug (bool): enable debug """ self._check_parameters([ { 'name': 'module_name', 'type': str, 'value': module_name }, { 'name': 'debug', 'type': bool, 'value': debug }, ]) # save log level in conf file if debug: self.cleep_conf.enable_module_debug(module_name) else: self.cleep_conf.disable_module_debug(module_name) # set debug on module if module_name == 'rpc': # specific command for rpcserver resp = self.send_command('set_rpc_debug', 'inventory', {'debug': debug}) else: resp = self.send_command('set_debug', module_name, {'debug': debug}) if resp.error: self.logger.error('Unable to set debug on module %s: %s' % (module_name, resp.message)) raise CommandError('Update debug failed')
def generate_documentation(self, module_name): """ Generate documentation Args: module_name (string): module name """ if self.__docs_task: raise CommandError( 'Doc generation is running. Please wait end of it') cmd = self.CLI_DOCS_CMD % (self.CLI, module_name) self.logger.debug('Doc generation cmd: %s' % cmd) self.__docs_task = EndlessConsole(cmd, self.__docs_callback, self.__docs_end_callback) self.__docs_task.start() self.docs_output_event.send(params={ 'messages': 'Documentation generation started. Please wait...' }, to='rpc', render=False)
def tweak_activity_led(self, enable): """ Tweak raspberry pi activity led Args: enable (bool): True to turn on led """ if not os.path.exists('/sys/class/leds/led0'): self.logger.info('Activity led not found on this device') return raspi = Tools.raspberry_pi_infos() off_value = '0' if raspi['model'].lower().find('zero') else '1' on_value = '1' if raspi['model'].lower().find('zero') else '0' echo_value = on_value if enable else off_value console = Console() resp = console.command('echo %s > /sys/class/leds/led0/brightness' % echo_value) if resp['returncode'] != 0: raise CommandError('Error tweaking activity led') # store led status self._set_config_field('enableactivityled', enable)
def set_position(self, latitude, longitude): """ Set device position Args: latitude (float): latitude longitude (float): longitude Raises: CommandError: if error occured during position saving """ if latitude is None: raise MissingParameter('Parameter "latitude" is missing') if not isinstance(latitude, float): raise InvalidParameter('Parameter "latitude" is invalid') if longitude is None: raise MissingParameter('Parameter "longitude" is missing') if not isinstance(longitude, float): raise InvalidParameter('Parameter "longitude" is invalid') # save new position position = {"latitude": latitude, "longitude": longitude} if not self._set_config_field("position", position): raise CommandError("Unable to save position") # reset python time to take into account last modifications before # computing new times time.tzset() # and update related stuff self.set_timezone() self.set_country() self.set_sun() # send now event self._time_task()
def set_timezone(self): """ Set timezone according to coordinates Returns: bool: True if function succeed, False otherwise Raises: CommandError: if unable to save timezone """ # get position position = self._get_config_field("position") if not position["latitude"] and not position["longitude"]: self.logger.warning( "Unable to set timezone from unspecified position (%s)" % position) return False # compute timezone current_timezone = None try: # try to find timezone at position current_timezone = self.timezonefinder.timezone_at( lat=position["latitude"], lng=position["longitude"]) if current_timezone is None: # extend search to closest position # TODO increase delta_degree to extend research, careful it use more CPU ! current_timezone = self.timezonefinder.closest_timezone_at( lat=position["latitude"], lng=position["longitude"]) except ValueError: # the coordinates were out of bounds self.logger.exception("Coordinates out of bounds") except Exception: self.logger.exception( "Error occured searching timezone at position") if not current_timezone: self.logger.warning( "Unable to set device timezone because it was not found") return False # save timezone value self.logger.debug("Save new timezone: %s" % current_timezone) if not self._set_config_field("timezone", current_timezone): raise CommandError("Unable to save timezone") # configure system timezone zoneinfo = os.path.join(self.SYSTEM_ZONEINFO_DIR, current_timezone) self.logger.debug("Checking zoneinfo file: %s" % zoneinfo) if not os.path.exists(zoneinfo): raise CommandError('No system file found for "%s" timezone' % current_timezone) self.logger.debug('zoneinfo file "%s" exists' % zoneinfo) self.cleep_filesystem.rm(self.SYSTEM_LOCALTIME) self.logger.debug('Writing timezone "%s" in "%s"' % (current_timezone, self.SYSTEM_TIMEZONE)) if not self.cleep_filesystem.write_data(self.SYSTEM_TIMEZONE, "%s" % current_timezone): self.logger.error( 'Unable to write timezone data on "%s". System timezone is not configured!' % self.SYSTEM_TIMEZONE) return False # launch timezone update in background self.logger.debug("Updating system timezone") command = Console() res = command.command( "/usr/sbin/dpkg-reconfigure -f noninteractive tzdata", timeout=15.0) self.logger.debug("Timezone update command result: %s" % res) if res["returncode"] != 0: self.logger.error("Error reconfiguring system timezone: %s" % res["stderr"]) return False # TODO configure all wpa_supplicant.conf country code # propagate changes to cleep self.timezone = timezone(current_timezone) self._time_task() return True
def save_wired_static_configuration(self, interface_name, ip_address, gateway, netmask, fallback): """ Save wired static configuration Args: interface_name (string): interface name to configure ip_address (string): desired ip address gateway (string): gateway address netmask (string): netmask fallback (bool): is configuration used as fallback (>=jessie) """ # check params self._check_parameters([ {'name': 'interface_name', 'value': interface_name, 'type': str}, {'name': 'ip_address', 'value': ip_address, 'type': str}, {'name': 'gateway', 'value': gateway, 'type': str}, {'name': 'netmask', 'value': netmask, 'type': str}, {'name': 'fallback', 'value': fallback, 'type': bool}, ]) # add new one if self.dhcpcd.is_installed(): # use dhcpcd # delete existing configuration for specified interface if exists self.dhcpcd.delete_interface(interface_name) # add new configuration if not fallback: if not self.dhcpcd.add_static_interface(interface_name, ip_address, gateway, netmask): self.logger.error( 'Unable to save wired static configuration (dhcpcd): unable to add interface %s' % interface_name ) raise CommandError('Unable to save configuration') else: if not self.dhcpcd.add_fallback_interface(interface_name, ip_address, gateway, netmask): self.logger.error( 'Unable to save wired fallback configuration (dhcpcd): unable to add interface %s' % interface_name ) raise CommandError('Unable to save configuration') else: # use /etc/network/interfaces file # delete existing configuration for specified interface if exists self.etcnetworkinterfaces.delete_interface(interface_name) # finally add new configuration if not self.etcnetworkinterfaces.add_static_interface( interface_name, EtcNetworkInterfaces.OPTION_HOTPLUG, ip_address, gateway, netmask): self.logger.error( 'Unable to save wired static configuration (interfaces): unable to add interface %s' % interface_name ) raise CommandError('Unable to save configuration') # restart interface self.reconfigure_wired_interface(interface_name)