def read(self): """Parse and validate the config file. The read data is accessible as a dictionary in this instance :return: None """ try: data = load(open(self.file), Loader) except (UnicodeDecodeError, YAMLError) as e: raise InvalidConfig(self.file, '{}'.format(e)) try: validate(data, SCHEMA) except ValidationError as e: raise InvalidConfig(self.file, e) self.update(data)
def get_url(self): """IFTTT Webhook url :return: url :rtype: str """ if not self.data[self.execute_name]: raise InvalidConfig(extra_body='Value for IFTTT is required on {} device. Get your key here: ' 'https://ifttt.com/services/maker_webhooks/settings'.format(self.name)) if not self.data.get('event'): raise InvalidConfig(extra_body='Event option is required for IFTTT on {} device. ' 'You define the event name when creating a Webhook ' 'applet'.format(self.name)) url = self.url_pattern.format(event=self.data['event'], key=self.data[self.execute_name]) return url
def execute_over_ssh(cmd, ssh, cwd=None, shell='bash'): """Excecute command on remote machine using SSH :param cmd: Command to execute :param ssh: Server to connect. Port is optional :param cwd: current working directory :return: None """ port = None parts = ssh.split(':', 1) if len(parts) > 1 and not parts[1].isdigit(): raise InvalidConfig(extra_body='Invalid port number on ssh config: {}'. format(parts[1])) elif len(parts) > 1: port = parts[1] quoted_cmd = ' '.join( [x.replace("'", """'"'"'""") for x in cmd.split(' ')]) remote_cmd = ' '.join( [ ' '.join(get_shell(shell)), # /usr/bin/env bash ' '.join([ EXECUTE_SHELL_PARAM, "'", ' '.join((['cd', cwd, ';'] if cwd else []) + [quoted_cmd]), "'" ]) ], ) return ['ssh', parts[0] ] + (['-p', port] if port else []) + ['-C'] + [remote_cmd]
def execute(self, root_allowed=False): """Execute using self.data :param bool root_allowed: Allow execute as root commands :return: """ if self.user == ROOT_USER and not root_allowed and not self.data.get( 'ssh'): raise SecurityException( 'For security, execute commands as root is not allowed. ' 'Use --root-allowed to allow executing commands as root. ' ' It is however recommended to add a user to the configuration ' 'of the device (device: {})'.format(self.name)) if self.data.get('user') and self.data.get('ssh'): raise InvalidConfig( 'User option is unsupported in ssh mode. The ssh user must be defined in ' 'the ssh option. For example: user@machine') if self.data.get('ssh'): cmd = execute_over_ssh(self.data['cmd'], self.data['ssh'], self.data.get('cwd')) output = execute_cmd(cmd) else: cmd = run_as_cmd(self.data['cmd'], self.user) output = execute_cmd(cmd, self.data.get('cwd')) if output: return output[0]
def get_confirmation(device_id, device_data, confirmations): name = device_data.get('confirmation') if name and name not in confirmations: raise InvalidConfig( extra_body='{} is not a registered confirmation config on {} device' .format(name, device_id)) if name: return get_confirmation_instance(confirmations[name]) defaults = list( filter(lambda x: x.get('is_default'), confirmations.values())) if len(defaults) > 1: raise InvalidConfig( extra_body='Multiple default confirmations. There can be only one.' ) if defaults: return get_confirmation_instance(defaults[0])
def __init__(self, data): for key in self.required_fields: if key not in data: raise InvalidConfig( extra_body='{} is a required parameter for {} confirmation' .format(key, self.name)) self.data = data
def __init__(self, src, data=None, config=None): """ :param str src: Mac address :param data: device data """ data = data or {} config = config or {} if isinstance(src, Device): src = src.src self.src = src.lower() self.data = data execs = [ cls(self.name, data) for name, cls in EXECUTE_CLS.items() if name in self.data ] if len(execs) > 1: raise InvalidConfig( extra_body= 'There can only be one method of execution on a device. The device is {}. ' .format(self.name)) elif len(execs): self.execute_instance = execs[0] self.execute_instance.validate() self.confirmation = get_confirmation(src, data, config.get('confirmations', {}))
def __init__(self, data): one_fields = set(data) & self.one_field_of if len(one_fields) > 1: raise InvalidConfig( extra_body='Only one in {} is required for {} notifications'. format(', '.join(one_fields), self.name)) elif one_fields: self.to_field = one_fields.pop() super(PushbulletConfirmation, self).__init__(data)
def get_url(self): """Open Hab url :return: url :rtype: str """ url = super(ExecuteOpenHab, self).get_url() if not self.data.get('item'): raise InvalidConfig(extra_body='Item option is required for Open Hab on {} device.'.format(self.name)) url += '/rest/items/{}'.format(self.data['item']) return url
def get_url(self): """Home assistant url :return: url :rtype: str """ url = super(ExecuteHomeAssistant, self).get_url() if not self.data.get('event'): raise InvalidConfig(extra_body='Event option is required for HomeAsistant on {} device.'.format(self.name)) url += '/api/events/{}'.format(self.data['event']) return url
def validate(self): """Check self.data. Raise InvalidConfig on error :return: None """ if (self.data.get('content-type') or self.data.get('body')) and \ self.data.get('method', '').lower() not in CONTENT_TYPE_METHODS: raise InvalidConfig( extra_body='The body/content-type option only can be used with the {} methods. The device is {}. ' 'Check the configuration file.'.format(', '.join(CONTENT_TYPE_METHODS), self.name) ) self.data['content-type'] = CONTENT_TYPE_ALIASES.get(self.data.get('content-type'), self.data.get('content-type')) form_type = CONTENT_TYPE_ALIASES['form'] if self.data.get('body') and (self.data.get('content-type') or form_type) == form_type: try: self.data['body'] = json.loads(self.data['body']) except JSONDecodeError: raise InvalidConfig( extra_body='Invalid JSON body on {} device.'.format(self.name) )
def get_url(self): """Home assistant url :return: url :rtype: str """ url = self.data['homeassistant'] parsed = urlparse(url) if not parsed.scheme: url = 'http://{}'.format(url) if not url.split(':')[-1].isalnum(): url += ':8123' if not self.data.get('event'): raise InvalidConfig( extra_body= 'Event option is required for HomeAsistant on {} device.'. format(self.name)) url += '/api/events/{}'.format(self.data['event']) return url
def get_confirmation_instance(confirmation_data): confirmation_data = confirmation_data.copy() if confirmation_data.get('service') not in CONFIRMATIONS: raise InvalidConfig(extra_body='{} is a invalid confirmation service') return CONFIRMATIONS[confirmation_data.pop('service')](confirmation_data)