Skip to content

Commit 86f7631

Browse files
author
Leopoldo
committed
Insert cumulus plugin
1 parent d96b0e1 commit 86f7631

1 file changed

Lines changed: 306 additions & 0 deletions

File tree

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
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

Comments
 (0)