diff --git a/lib/ansible/module_utils/openswitch.py b/lib/ansible/module_utils/openswitch.py new file mode 100644 index 0000000000..9ff7450ee7 --- /dev/null +++ b/lib/ansible/module_utils/openswitch.py @@ -0,0 +1,246 @@ +# +# (c) 2015 Peter Sprygada, +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +import time +import json + +try: + from runconfig import runconfig + from opsrest.settings import settings + from opsrest.manager import OvsdbConnectionManager + from opslib import restparser + HAS_OPS = True +except ImportError: + HAS_OPS = False + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(), + port=dict(type='int'), + username=dict(), + password=dict(no_log=True), + transport=dict(default='ssh', choices=['ssh', 'cli', 'rest']), +) + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + +def get_idl(): + manager = OvsdbConnectionManager(settings.get('ovs_remote'), + settings.get('ovs_schema')) + manager.start() + idl = manager.idl + + init_seq_no = 0 + while (init_seq_no == idl.change_seqno): + idl.run() + time.sleep(1) + + return idl + +def get_schema(): + return restparser.parseSchema(settings.get('ext_schema')) + +def get_runconfig(): + idl = get_idl() + schema = get_schema() + return runconfig.RunConfigUtil(idl, schema) + +class Response(object): + + def __init__(self, resp, hdrs): + self.body = resp.read() + self.headers = hdrs + + @property + def json(self): + try: + return json.loads(self.body) + except ValueError: + return None + +class Rest(object): + + def __init__(self, module): + self.module = module + self.baseurl = None + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.baseurl = '%s://%s:%s/rest/v1' % (proto, host, port) + + def _url_builder(self, path): + if path[0] == '/': + path = path[1:] + return '%s/%s' % (self.baseurl, path) + + def send(self, method, path, data=None, headers=None): + url = self._url_builder(path) + data = self.module.jsonify(data) + + if headers is None: + headers = dict() + headers.update({'Content-Type': 'application/json'}) + + resp, hdrs = fetch_url(self.module, url, data=data, headers=headers, + method=method) + + return Response(resp, hdrs) + + def get(self, path, data=None, headers=None): + return self.send('GET', path, data, headers) + + def put(self, path, data=None, headers=None): + return self.send('PUT', path, data, headers) + + def post(self, path, data=None, headers=None): + return self.send('POST', path, data, headers) + + def delete(self, path, data=None, headers=None): + return self.send('DELETE', path, data, headers) + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + + self.shell = Shell() + self.shell.open(host, port=port, username=username, password=password) + + def send(self, commands, encoding='text'): + return self.shell.send(commands) + +class OpsModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(OpsModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._runconfig = None + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def connect(self): + if self.params['transport'] == 'rest': + self.connection = Rest(self) + elif self.params['transport'] == 'cli': + self.connection = Cli(self) + + try: + self.connection.connect() + except Exception, exc: + self.fail_json(msg=exc.message) + + def configure(self, config): + if self.params['transport'] == 'cli': + commands = to_list(config) + commands.insert(0, 'configure terminal') + responses = self.execute(commands) + responses.pop(0) + return responses + elif self.params['transport'] == 'rest': + path = '/system/full-configuration' + return self.connection.put(path, data=config) + else: + if not self._runconfig: + self._runconfig = get_runconfig() + self._runconfig.write_config_to_db(config) + + def execute(self, commands, **kwargs): + try: + return self.connection.send(commands, **kwargs) + except Exception, exc: + self.fail_json(msg=exc.message, commands=commands) + + def disconnect(self): + self.connection.close() + + def parse_config(self, cfg): + return parse(cfg, indent=4) + + def get_config(self): + if self.params['transport'] == 'cli': + return self.execute('show running-config')[0] + + elif self.params['transport'] == 'rest': + resp = self.connection.get('/system/full-configuration') + return resp.json + + else: + if not self._runconfig: + self._runconfig = get_runconfig() + return self._runconfig.get_running_config() + + +def get_module(**kwargs): + """Return instance of OpsModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + kwargs['check_invalid_arguments'] = False + + module = OpsModule(**kwargs) + + if not HAS_OPS and module.params['transport'] == 'ssh': + module.fail_json(msg='could not import ops library') + + # HAS_PARAMIKO is set by module_utils/shell.py + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + # copy in values from local action. + params = json_dict_unicode_to_bytes(json.loads(MODULE_COMPLEX_ARGS)) + for key, value in params.iteritems(): + module.params[key] = value + + if module.params['transport'] in ['cli', 'rest']: + module.connect() + + return module + diff --git a/lib/ansible/utils/module_docs_fragments/openswitch.py b/lib/ansible/utils/module_docs_fragments/openswitch.py new file mode 100644 index 0000000000..1427fc7525 --- /dev/null +++ b/lib/ansible/utils/module_docs_fragments/openswitch.py @@ -0,0 +1,66 @@ +# +# (c) 2015, Peter Sprygada +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + +class ModuleDocFragment(object): + + # Standard files documentation fragment + DOCUMENTATION = """ +options: + host: + description: + - Specifies the DNS host name or address for connecting to the remote + device over the specified transport. The value of host is used as + the destination address for the transport. Note this argument + does not affect the SSH argument. + required: true + port: + description: + - Specifies the port to use when buiding the connection to the remote + device. This value applies to either I(cli) or I(). The port + value will default to the approriate transport common port if + none is provided in the task. (cli=22, http=80, https=443). Note + this argument does not affect the SSH transport. + required: false + default: 0 (use common port) + username: + description: + - Configures the usename to use to authenticate the connection to + the remote device. The value of I(username) is used to authenticate + either the CLI login or the eAPI authentication depending on which + transport is used. Note this argument does not affect the SSH + transport. + required: true + password: + description: + - Specifies the password to use when authentication the connection to + the remote device. This is a common argument used for either I(cli) + or I(rest) transports. Note this argument does not affect the SSH + transport + required: false + default: null + transport: + description: + - Configures the transport connection to use when connecting to the + remote device. The transport argument supports connectivity to the + device over ssh, cli or REST. + required: true + default: ssh + choices: ['ssh', 'cli', 'rest'] + +"""