|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | +# Licensed to the Apache Software Foundation (ASF) under one or more |
| 3 | +# contributor license agreements. See the NOTICE file distributed with |
| 4 | +# this work for additional information regarding copyright ownership. |
| 5 | +# The ASF licenses this file to You under the Apache License, Version 2.0 |
| 6 | +# (the "License"); you may not use this file except in compliance with |
| 7 | +# the License. You may obtain a copy of the License at |
| 8 | +# |
| 9 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | +# |
| 11 | +# Unless required by applicable law or agreed to in writing, software |
| 12 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | +# See the License for the specific language governing permissions and |
| 15 | +# limitations under the License. |
| 16 | + |
| 17 | +import re |
| 18 | +import logging |
| 19 | +import httplib2 |
| 20 | +import socket |
| 21 | +from json import dumps |
| 22 | +from time import sleep |
| 23 | + |
| 24 | +from rest_framework import status |
| 25 | +from CumulusExceptions import * |
| 26 | +from networkapi.equipamento.models import EquipamentoAcesso |
| 27 | +from networkapi.settings import TFTPBOOT_FILES_PATH |
| 28 | +from ..base import BasePlugin |
| 29 | +from .. import exceptions |
| 30 | + |
| 31 | + |
| 32 | +log = logging.getLogger(__name__) |
| 33 | + |
| 34 | + |
| 35 | +class Cumulus(BasePlugin): |
| 36 | + """Cumulus Plugin""" |
| 37 | + # httplib2 configurations |
| 38 | + HTTP = httplib2.Http('.cache', disable_ssl_certificate_validation=True) |
| 39 | + HEADERS = {'Content-Type': 'application/json; charset=UTF-8'} |
| 40 | + httplib2.RETRIES = 3 |
| 41 | + |
| 42 | + # Cumulus commands to control the staging area |
| 43 | + COMMIT = {'cmd': 'commit'} |
| 44 | + ABORT_CHANGES = {'cmd': 'abort'} |
| 45 | + PENDING = {'cmd': 'pending'} |
| 46 | + # Expected strings when something not expected occurs |
| 47 | + WARNINGS = 'WARNING: Committing these changes will cause problems' |
| 48 | + COMMIT_CONCURRENCY = 'Multiple users are currently' |
| 49 | + COMMON_USERS = 'cumulus|root' |
| 50 | + COMMIT_ERRORS = 'error:|returned non-zero exit status' |
| 51 | + ALREADY_EXISTS = 'configuration already has' |
| 52 | + CLI_ERROR = 'ERROR:' |
| 53 | + # Variables needed in the configuration below |
| 54 | + MAX_WAIT = 5 |
| 55 | + MAX_RETRIES = 3 |
| 56 | + SLEEP_WAIT_TIME = 5 |
| 57 | + _command_list = list() |
| 58 | + device = None |
| 59 | + |
| 60 | + def _get_info(self): |
| 61 | + """Get info from database to access the device""" |
| 62 | + if self.equipment_access is None: |
| 63 | + try: |
| 64 | + self.equipment_access = EquipamentoAcesso.search( |
| 65 | + None, self.equipment, 'https').uniqueResult() |
| 66 | + except Exception: |
| 67 | + log.error('Access type %s not found for equipment %s.' % |
| 68 | + ('https', self.equipment.nome)) |
| 69 | + raise exceptions.InvalidEquipmentAccessException() |
| 70 | + |
| 71 | + self.device = self.equipment_access.fqdn |
| 72 | + username = self.equipment_access.user |
| 73 | + password = self.equipment_access.password |
| 74 | + |
| 75 | + self.HTTP.add_credentials(username, password) |
| 76 | + |
| 77 | + def connect(self): |
| 78 | + """Use the connect function of the superclass to get |
| 79 | + the informations for access the device""" |
| 80 | + self._get_info() |
| 81 | + |
| 82 | + def _get_conf_from_file(self, filename): |
| 83 | + """Get the configurations needed to be applied |
| 84 | + and insert into a list |
| 85 | +
|
| 86 | + TFTPBOOT_FILES_PATH is required so we can read |
| 87 | + the generated config file |
| 88 | + """ |
| 89 | + try: |
| 90 | + with open(TFTPBOOT_FILES_PATH + filename, 'r+') as lines: |
| 91 | + for line in lines: |
| 92 | + self._command_list.append(line) |
| 93 | + except IOError as e: |
| 94 | + log.error('Error opening the file: %s' % filename) |
| 95 | + raise e |
| 96 | + except Exception as e: |
| 97 | + log.error("Error %s when trying to " |
| 98 | + "read the file %s" % (e, filename)) |
| 99 | + raise e |
| 100 | + return True |
| 101 | + |
| 102 | + def _send_request(self, data, uri): |
| 103 | + """Send requests for the equipment""" |
| 104 | + try: |
| 105 | + count = 0 |
| 106 | + while count < self.MAX_RETRIES: |
| 107 | + resp, content = self.HTTP.request(uri, |
| 108 | + method="POST", |
| 109 | + headers=self.HEADERS, |
| 110 | + body=dumps(data)) |
| 111 | + if resp.status == status.HTTP_200_OK: |
| 112 | + return content |
| 113 | + count += 1 |
| 114 | + if count >= self.MAX_RETRIES: |
| 115 | + raise MaxRetryAchieved(self.equipment.nome) |
| 116 | + except MaxRetryAchieved as error: |
| 117 | + log.error(error) |
| 118 | + raise error |
| 119 | + except socket.error as error: |
| 120 | + log.error('Error in socket connection: %s' % error) |
| 121 | + raise error |
| 122 | + except httplib2.ServerNotFoundError as error: |
| 123 | + log.error( |
| 124 | + 'Error: %s. Check if the restserver is enabled in %s' % |
| 125 | + (error, self.equipment.nome)) |
| 126 | + raise error |
| 127 | + except Exception as error: |
| 128 | + log.error('Error: %s' % error) |
| 129 | + raise error |
| 130 | + |
| 131 | + def _send_nclu_request(self, data): |
| 132 | + """Send requests to equipment using nclu route""" |
| 133 | + schema = 'https://' |
| 134 | + path = ':8080/nclu/v1/rpc' |
| 135 | + uri = schema + self.device + path |
| 136 | + |
| 137 | + output = self._send_request(data, uri) |
| 138 | + |
| 139 | + return output |
| 140 | + |
| 141 | + def _search_pending_warnings(self): |
| 142 | + """Validate if exists any warnings in the staging configuration""" |
| 143 | + try: |
| 144 | + content = self._send_nclu_request(self.PENDING) |
| 145 | + check_warning = re.search(self.WARNINGS, |
| 146 | + content, |
| 147 | + flags=re.IGNORECASE) |
| 148 | + if check_warning: |
| 149 | + self._send_nclu_request(self.ABORT_CHANGES) |
| 150 | + raise ConfigurationWarning() |
| 151 | + else: |
| 152 | + return True |
| 153 | + except ConfigurationWarning as error: |
| 154 | + log.error(error) |
| 155 | + raise error |
| 156 | + except Exception as error: |
| 157 | + log.error('Error: %s' % error) |
| 158 | + raise error |
| 159 | + |
| 160 | + def _search_commit_errors(self): |
| 161 | + """Look for errors fired when |
| 162 | + trying to commit configurations""" |
| 163 | + try: |
| 164 | + content = self._send_nclu_request(self.COMMIT) |
| 165 | + check_error = re.search(self.COMMIT_ERRORS, |
| 166 | + content, |
| 167 | + flags=re.IGNORECASE) |
| 168 | + if check_error: |
| 169 | + self._send_nclu_request(self.ABORT_CHANGES) |
| 170 | + raise CommitError() |
| 171 | + return content |
| 172 | + except CommitError as error: |
| 173 | + log.error(error) |
| 174 | + raise error |
| 175 | + except Exception as error: |
| 176 | + log.error(error) |
| 177 | + raise error |
| 178 | + |
| 179 | + def _check_pending(self): |
| 180 | + """Verify if exists any configuration in the staging area |
| 181 | + made by another user""" |
| 182 | + try: |
| 183 | + count = 0 |
| 184 | + while count < self.MAX_WAIT: |
| 185 | + content = self._send_nclu_request(self.PENDING) |
| 186 | + |
| 187 | + check_concurrency = re.search(self.COMMIT_CONCURRENCY, |
| 188 | + content, |
| 189 | + flags=re.IGNORECASE) |
| 190 | + |
| 191 | + check_users = re.search(self.COMMON_USERS, |
| 192 | + content) |
| 193 | + |
| 194 | + if check_users or check_concurrency: |
| 195 | + log.warning( |
| 196 | + 'The configuration staging for %s is been used' % |
| 197 | + self.equipment.nome) |
| 198 | + count += 1 |
| 199 | + if count >= self.MAX_WAIT: |
| 200 | + raise MaxTimeWaitExceeded(self.equipment.nome) |
| 201 | + sleep(self.SLEEP_WAIT_TIME) |
| 202 | + else: |
| 203 | + return True |
| 204 | + except MaxTimeWaitExceeded as error: |
| 205 | + log.error(error) |
| 206 | + raise error |
| 207 | + except Exception as error: |
| 208 | + log.error('Error: %s' % error) |
| 209 | + raise error |
| 210 | + |
| 211 | + def configurations(self): |
| 212 | + """Apply the configurations in equipment |
| 213 | + and search for errors syntax, and if the configurations |
| 214 | + will cause problems in the equipment""" |
| 215 | + try: |
| 216 | + proceed = self._check_pending() |
| 217 | + if proceed: |
| 218 | + for cmd in self._command_list: |
| 219 | + content = self._send_nclu_request({'cmd': cmd}) |
| 220 | + check_error = re.search(self.CLI_ERROR, |
| 221 | + content, |
| 222 | + flags=re.IGNORECASE) |
| 223 | + check_existence = re.search(self.ALREADY_EXISTS, |
| 224 | + content, |
| 225 | + flags=re.IGNORECASE) |
| 226 | + if check_error: |
| 227 | + self._send_nclu_request(self.ABORT_CHANGES) |
| 228 | + raise ConfigurationError(cmd) |
| 229 | + elif check_existence: |
| 230 | + log.info( |
| 231 | + 'The command "%s" already exists in %s' % |
| 232 | + (cmd, self.equipment.nome)) |
| 233 | + check_warnings = self._search_pending_warnings() |
| 234 | + if check_warnings: |
| 235 | + content = self._search_commit_errors() |
| 236 | + return content |
| 237 | + except ConfigurationError as error: |
| 238 | + log.error(error) |
| 239 | + raise error |
| 240 | + except Exception as error: |
| 241 | + log.error('Error: %s ' % error) |
| 242 | + raise error |
| 243 | + |
| 244 | + def copyScriptFileToConfig(self, filename, use_vrf='', destination=''): |
| 245 | + """Get the configurations needed for configure the equipment |
| 246 | + from the file generated |
| 247 | +
|
| 248 | + The use_vrf and destination variables won't be used |
| 249 | + """ |
| 250 | + try: |
| 251 | + success = self._get_conf_from_file(filename) |
| 252 | + if success: |
| 253 | + output = self.configurations() |
| 254 | + return output |
| 255 | + except Exception as error: |
| 256 | + log.error('Error: %s' % error) |
| 257 | + raise error |
| 258 | + |
| 259 | + def create_svi(self, svi_number, svi_description='no description'): |
| 260 | + """Create SVI in switch.""" |
| 261 | + try: |
| 262 | + proceed = self._check_pending() |
| 263 | + if proceed: |
| 264 | + command = "add vlan %s alias %s" % (svi_number, |
| 265 | + svi_description) |
| 266 | + self._send_nclu_request({'cmd': command}) |
| 267 | + check_warnings = self._search_pending_warnings() |
| 268 | + if check_warnings: |
| 269 | + output = self._search_commit_errors() |
| 270 | + return output |
| 271 | + except Exception as error: |
| 272 | + log.error('Error: %s' % error) |
| 273 | + raise error |
| 274 | + |
| 275 | + def remove_svi(self, svi_number): |
| 276 | + """Delete SVI from switch.""" |
| 277 | + try: |
| 278 | + proceed = self._check_pending() |
| 279 | + if proceed: |
| 280 | + command = "del vlan %s" % svi_number |
| 281 | + self._send_nclu_request({'cmd': command}) |
| 282 | + check_warnings = self._search_pending_warnings() |
| 283 | + if check_warnings: |
| 284 | + output = self._search_commit_errors() |
| 285 | + return output |
| 286 | + except Exception as error: |
| 287 | + log.error('Error: %s' % error) |
| 288 | + raise error |
| 289 | + |
| 290 | + def ensure_privilege_level(self, privilege_level=None): |
| 291 | + """Cumulus don't use the concept of privilege level""" |
| 292 | + pass |
| 293 | + |
| 294 | + def close(self): |
| 295 | + """This configuration file won't use ssh connections""" |
| 296 | + del self._command_list[:] |
| 297 | + pass |
| 298 | + |
| 299 | + def exec_command( |
| 300 | + self, |
| 301 | + command, |
| 302 | + success_regex='', |
| 303 | + invalid_regex=None, |
| 304 | + error_regex=None): |
| 305 | + """The exec command will not be needed here""" |
| 306 | + pass |
0 commit comments