class DellN3000(Switch): api_name = 'http://schema.massopencloud.org/haas/v0/switches/' \ 'delln3000' __mapper_args__ = { 'polymorphic_identity': api_name, } id = db.Column(db.Integer, db.ForeignKey('switch.id'), primary_key=True) hostname = db.Column(db.String, nullable=False) username = db.Column(db.String, nullable=False) password = db.Column(db.String, nullable=False) dummy_vlan = db.Column(db.String, nullable=False) @staticmethod def validate(kwargs): schema.Schema({ 'username': basestring, 'hostname': basestring, 'password': basestring, 'dummy_vlan': schema.And(schema.Use(int), lambda v: 0 < v and v <= 4093, schema.Use(str)), }).validate(kwargs) def session(self): return _DellN3000Session.connect(self)
class Nexus(Switch): """A Cisco Nexus switch.""" api_name = 'http://schema.massopencloud.org/haas/v0/switches/nexus' __mapper_args__ = { 'polymorphic_identity': api_name, } id = db.Column(BigIntegerType, db.ForeignKey('switch.id'), primary_key=True) hostname = db.Column(db.String, nullable=False) username = db.Column(db.String, nullable=False) password = db.Column(db.String, nullable=False) dummy_vlan = db.Column(db.String, nullable=False) @staticmethod def validate(kwargs): schema.Schema({ 'username': basestring, 'hostname': basestring, 'password': basestring, 'dummy_vlan': schema.And(schema.Use(int), lambda v: 0 <= v and v <= 4096, schema.Use(str)), }).validate(kwargs) def session(self): return _Session.connect(self) @staticmethod def validate_port_name(port): """ Valid port names for this switch are of the form: Ethernet1/12, ethernet1/12, Ethernet1/0/10, or ethernet1/0/10 """ val = re.compile(r'^(E|e)thernet\d+/\d+(/\d+)?$') if not val.match(port): raise BadArgumentError("Invalid port name. Valid port names for " "this switch are of the form: Ethernet1/12" " ethernet1/12, Ethernet1/0/10, or" " ethernet1/0/10") return def get_capabilities(self): return ['nativeless-trunk-mode']
class DellN3000(Switch): """Dell N3000 switch.""" api_name = 'http://schema.massopencloud.org/haas/v0/switches/' \ 'delln3000' __mapper_args__ = { 'polymorphic_identity': api_name, } id = db.Column(BigIntegerType, db.ForeignKey('switch.id'), primary_key=True) hostname = db.Column(db.String, nullable=False) username = db.Column(db.String, nullable=False) password = db.Column(db.String, nullable=False) dummy_vlan = db.Column(db.String, nullable=False) @staticmethod def validate(kwargs): schema.Schema({ 'username': basestring, 'hostname': basestring, 'password': basestring, 'dummy_vlan': schema.And(schema.Use(int), lambda v: 0 < v and v <= 4093, schema.Use(str)), }).validate(kwargs) def session(self): return _DellN3000Session.connect(self) @staticmethod def validate_port_name(port): """ Valid port names for this switch are of the form gi1/0/11, te1/0/12, gi1/12, or te1/3 """ val = re.compile(r'^(gi|te)\d+/\d+(/\d+)?$') if not val.match(port): raise BadArgumentError("Invalid port name. Valid port names for " "this switch are of the form gi1/0/11, " "te1/0/12, gi1/12, or te1/3") return
class PowerConnect55xx(Switch): """Dell powerconnect 5500 series switch.""" api_name = 'http://schema.massopencloud.org/haas/v0/switches/' \ 'powerconnect55xx' __mapper_args__ = { 'polymorphic_identity': api_name, } id = db.Column(BigIntegerType, db.ForeignKey('switch.id'), primary_key=True) hostname = db.Column(db.String, nullable=False) username = db.Column(db.String, nullable=False) password = db.Column(db.String, nullable=False) @staticmethod def validate(kwargs): Schema({ 'username': basestring, 'hostname': basestring, 'password': basestring, }).validate(kwargs) def session(self): return _PowerConnect55xxSession.connect(self) @staticmethod def validate_port_name(port): """ Valid port names for this switch are of the form gi1/0/11, te1/0/12, gi1/12, or te1/3 """ val = re.compile(r'^(gi|te)\d+/\d+(/\d+)?$') if not val.match(port): raise BadArgumentError("Invalid port name. Valid port names for " "this switch are of the form gi1/0/11, " "te1/0/12, gi1/12, or te1/3") return def get_capabilities(self): return ['nativeless-trunk-mode']
class PowerConnect55xx(Switch): api_name = 'http://schema.massopencloud.org/haas/v0/switches/' \ 'powerconnect55xx' __mapper_args__ = { 'polymorphic_identity': api_name, } id = db.Column(db.Integer, db.ForeignKey('switch.id'), primary_key=True) hostname = db.Column(db.String, nullable=False) username = db.Column(db.String, nullable=False) password = db.Column(db.String, nullable=False) @staticmethod def validate(kwargs): schema.Schema({ 'username': basestring, 'hostname': basestring, 'password': basestring, }).validate(kwargs) def session(self): return _PowerConnect55xxSession.connect(self)
self.is_admin = is_admin self.set_password(password) def verify_password(self, password): """Return whether `password` is the user's (plaintext) password.""" return sha512_crypt.verify(password, self.hashed_password) def set_password(self, password): """Set the user's password to `password` (which must be plaintext).""" self.hashed_password = sha512_crypt.encrypt(password) # A joining table for users and projects, which have a many to many # relationship: user_projects = db.Table('user_projects', db.Column('user_id', db.ForeignKey('user.id')), db.Column('project_id', db.ForeignKey('project.id'))) @rest_call('PUT', '/auth/basic/user/<user>', schema=Schema({ 'user': basestring, 'password': basestring, Optional('is_admin'): bool, }), dont_log=('password', )) def user_create(user, password, is_admin=False): """Create user with given password. If the user already exists, a DuplicateError will be raised.
class DellNOS9(Switch, SwitchSession): """Dell S3048-ON running Dell NOS9""" api_name = 'http://schema.massopencloud.org/haas/v0/switches/dellnos9' __mapper_args__ = { 'polymorphic_identity': api_name, } id = db.Column(BigIntegerType, db.ForeignKey('switch.id'), primary_key=True) hostname = db.Column(db.String, nullable=False) username = db.Column(db.String, nullable=False) password = db.Column(db.String, nullable=False) interface_type = db.Column(db.String, nullable=False) @staticmethod def validate(kwargs): schema.Schema({ 'hostname': basestring, 'username': basestring, 'password': basestring, 'interface_type': basestring, }).validate(kwargs) def session(self): return self def ensure_legal_operation(self, nic, op_type, channel): # get the network attachments for <nic> from the database table = model.NetworkAttachment query = db.session.query(table).filter(table.nic_id == nic.id) if channel != 'vlan/native' and op_type == 'connect' and \ query.filter(table.channel == 'vlan/native').count() == 0: # checks if it is trying to attach a trunked network, and then in # in the db see if nic does not have any networks attached natively raise BlockedError("Please attach a native network first") elif channel == 'vlan/native' and op_type == 'detach' and \ query.filter(table.channel != 'vlan/native').count() > 0: # if it is detaching a network, then check in the database if there # are any trunked vlans. raise BlockedError("Please remove all trunked Vlans" " before removing the native vlan") else: return @staticmethod def validate_port_name(port): """Valid port names for this switch are of the form 1/0/1 or 1/2""" if not re.match(r'^\d+/\d+(/\d+)?$', port): raise BadArgumentError("Invalid port name. Valid port names for " "this switch are of the form 1/0/1 or 1/2") return def disconnect(self): """Since the switch is not connection oriented, we don't need to establish a session or disconnect from it.""" def modify_port(self, port, channel, new_network): (port,) = filter(lambda p: p.label == port, self.ports) interface = port.label if channel == 'vlan/native': if new_network is None: self._remove_native_vlan(interface) self._port_shutdown(interface) else: self._set_native_vlan(interface, new_network) else: vlan_id = channel.replace('vlan/', '') legal = get_network_allocator(). \ is_legal_channel_for(channel, vlan_id) assert legal, "HIL passed an invalid channel to the switch!" if new_network is None: self._remove_vlan_from_trunk(interface, vlan_id) else: assert new_network == vlan_id self._add_vlan_to_trunk(interface, vlan_id) def revert_port(self, port): self._remove_all_vlans_from_trunk(port) if self._get_native_vlan(port) is not None: self._remove_native_vlan(port) self._port_shutdown(port) def get_port_networks(self, ports): response = {} for port in ports: response[port] = self._get_vlans(port.label) native = self._get_native_vlan(port.label) if native is not None: response[port].append(native) return response def _get_vlans(self, interface): """ Return the vlans of a trunk port. Does not include the native vlan. Use _get_native_vlan. Args: interface: interface to return the vlans of Returns: List containing the vlans of the form: [('vlan/vlan1', vlan1), ('vlan/vlan2', vlan2)] """ # It uses the REST API CLI which is slow but it is the only way # because the switch is VLAN centric. Doing a GET on interface won't # return the VLANs on it, we would have to do get on all vlans (if that # worked reliably in the first place) and then find our interface there # which is not feasible. if not self._is_port_on(interface): return [] response = self._get_port_info(interface) # finds a comma separated list of integers and/or ranges starting with # T. Sample T12,14-18,23,28,80-90 or T20 or T20,22 or T20-22 match = re.search(r'T(\d+(-\d+)?)(,\d+(-\d+)?)*', response) if match is None: return [] range_str = match.group().replace('T', '').split(',') vlan_list = [] # need to interpret the ranges to numbers. e.g. 14-18 as 14,15,16,17,18 for num_str in range_str: if '-' in num_str: num_str = num_str.split('-') for x in range(int(num_str[0]), int(num_str[1])+1): vlan_list.append(str(x)) else: vlan_list.append(num_str) return [('vlan/%s' % x, x) for x in vlan_list] def _get_native_vlan(self, interface): """ Return the native vlan of an interface. Args: interface: interface to return the native vlan of Returns: Tuple of the form ('vlan/native', vlan) or None Similar to _get_vlans() """ if not self._is_port_on(interface): return None response = self._get_port_info(interface) match = re.search(r'NativeVlanId:(\d+)\.', response) if match is not None: vlan = match.group(1) else: logger.error('Unexpected: No native vlan found') return # FIXME: At this point, we are unable to remove the default native vlan # The switchport defaults to native vlan 1. Our deployment tests make # assertions that a switchport has no default vlan at start. # To get the tests to pass, we return None if we see the switchport has # a default native vlan (=1). This needs to be fixed ASAP. if vlan == '1': return None return ('vlan/native', vlan) def _get_port_info(self, interface): """Returns the output of a show interface command. This removes all spaces from the response before returning it which is then parsed by the caller. Sample Response: u"<outputxmlns='http://www.dell.com/ns/dell:0.1/root'>\n <command>show interfaces switchport GigabitEthernet1/3\r\n\r\n Codes: U-Untagged T-Tagged\r\n x-Dot1x untagged,X-Dot1xtagged\r\n G-GVRP tagged,M-Trunk\r\n i-Internal untagged, I-Internaltagged, v-VLTuntagged, V-VLTtagged\r\n\r\n Name:GigabitEthernet1/3\r\n 802.1Q Tagged:Hybrid\r\n Vlan membership:\r\n Q Vlans\r\n U 1512 \r\n T 1511 1612-1614,1700\r\n\r\n Native Vlan Id: 1512.\r\n\r\n\r\n\r\n MOC-Dell-S3048-ON#</command>\n</output>\n" """ command = 'interfaces switchport %s %s' % \ (self.interface_type, interface) response = self._execute(interface, SHOW, command) return response.text.replace(' ', '') def _add_vlan_to_trunk(self, interface, vlan): """ Add a vlan to a trunk port. If the port is not trunked, its mode will be set to trunk. Args: interface: interface to add the vlan to vlan: vlan to add """ if not self._is_port_on(interface): self._port_on(interface) command = 'interface vlan ' + vlan + '\r\n tagged ' + \ self.interface_type + ' ' + interface self._execute(interface, CONFIG, command) def _remove_vlan_from_trunk(self, interface, vlan): """ Remove a vlan from a trunk port. Args: interface: interface to remove the vlan from vlan: vlan to remove """ command = self._remove_vlan_command(interface, vlan) self._execute(interface, CONFIG, command) def _remove_all_vlans_from_trunk(self, interface): """ Remove all vlan from a trunk port. Args: interface: interface to remove the vlan from """ command = '' for vlan in self._get_vlans(interface): command += self._remove_vlan_command(interface, vlan[1]) + '\r\n ' # execute command only if there are some vlans to remove, otherwise # the switch complains if command is not '': self._execute(interface, CONFIG, command) def _remove_vlan_command(self, interface, vlan): """Returns command to remove <vlan> from <interface>""" return 'interface vlan ' + vlan + '\r\n no tagged ' + \ self.interface_type + ' ' + interface def _set_native_vlan(self, interface, vlan): """ Set the native vlan of an interface. Args: interface: interface to set the native vlan to vlan: vlan to set as the native vlan Method relies on the REST API CLI which is slow """ if not self._is_port_on(interface): self._port_on(interface) command = 'interface vlan ' + vlan + '\r\n untagged ' + \ self.interface_type + ' ' + interface self._execute(interface, CONFIG, command) def _remove_native_vlan(self, interface): """ Remove the native vlan from an interface. Args: interface: interface to remove the native vlan from.vlan """ try: vlan = self._get_native_vlan(interface)[1] command = 'interface vlan ' + vlan + '\r\n no untagged ' + \ self.interface_type + ' ' + interface self._execute(interface, CONFIG, command) except TypeError: logger.error('No native vlan to remove') def _port_shutdown(self, interface): """ Shuts down <interface> Turn off portmode hybrid, disable switchport, and then shut down the port. All non-default vlans must be removed before calling this. """ url = self._construct_url(interface=interface) interface = self._convert_interface_type(self.interface_type) + \ interface.replace('/', '-') payload = '<interface><name>%s</name><portmode><hybrid>false' \ '</hybrid></portmode><shutdown>true</shutdown>' \ '</interface>' % interface self._make_request('PUT', url, data=payload) def _port_on(self, interface): """ Turns on <interface> Turn on port and enable hybrid portmode and switchport. """ url = self._construct_url(interface=interface) interface = self._convert_interface_type(self.interface_type) + \ interface.replace('/', '-') payload = '<interface><name>%s</name><portmode><hybrid>true' \ '</hybrid></portmode><switchport></switchport>' \ '<shutdown>false</shutdown></interface>' % interface self._make_request('PUT', url, data=payload) def _is_port_on(self, port): """ Returns a boolean that tells the status of a switchport""" # the url here requires a suffix to GET the shutdown tag in response. url = self._construct_url(interface=port) + '\?with-defaults' response = self._make_request('GET', url) root = etree.fromstring(response.text) shutdown = root.find(self._construct_tag('shutdown')).text assert shutdown in ('false', 'true'), "unexpected state of switchport" return shutdown == 'false' # HELPER METHODS ********************************************* def _execute(self, interface, command_type, command): """This method gets the url & the payload and executes <command>""" url = self._construct_url() payload = self._make_payload(command_type, command) return self._make_request('POST', url, data=payload) def _construct_url(self, interface=None): """ Construct the API url for a specific interface. Args: interface: interface to construct the url for Returns: string with the url for a specific interface and operation If interface is None, then it returns the URL for REST API CLI. """ if interface is None: return '%s/api/running/dell/_operations/cli' % self.hostname try: self.validate_port_name(interface) # if `interface` refers to port name # the urls have dashes instead of slashes in interface names interface = interface.replace('/', '-') interface_type = self._convert_interface_type(self.interface_type) except BadArgumentError: # interface refers to `vlan` interface_type = 'vlan-' return ''.join([self.hostname, '/api/running/dell/interfaces/' 'interface/', interface_type, interface]) @staticmethod def _convert_interface_type(interface_type): """ Convert the interface type from switch CLI form to what the API server understands. Args: interface: the interface in the CLI-form Returns: string interface """ iftypes = {'GigabitEthernet': 'gige-', 'TenGigabitEthernet': 'tengig-', 'TwentyfiveGigabitEthernet': 'twentyfivegig-', 'fortyGigE': 'fortygig-', 'peGigabitEthernet': 'pegig-', 'FiftyGigabitEthernet': 'fiftygig-', 'HundredGigabitEthernet': 'hundredgig-'} return iftypes[interface_type] @property def _auth(self): return self.username, self.password @staticmethod def _make_payload(command, command_type): """Makes payload for passing CLI commands using the REST API""" return '<input><%s>%s</%s></input>' % (command, command_type, command) @staticmethod def _construct_tag(name): """ Construct the xml tag by prepending the dell tag prefix. """ return '{http://www.dell.com/ns/dell:0.1/root}%s' % name def _make_request(self, method, url, data=None): r = requests.request(method, url, data=data, auth=self._auth) if r.status_code >= 400: logger.error('Bad Request to switch. Response: %s', r.text) return r
class DeferredTestSwitch_(Switch): '''DeferredTestSwitch This is a switch implemented to test the deferred.apply_networking() function. It is needed for a custom implementation of the switch's apply_networking() that counts the networking actions as apply_networking() is called to ensure it is incremental. It is defined as a fixture because if it is defined as a class with global scope it's going to be defined for every test; so this will be picked up by migration tests and those would fail because there will be no migration scripts for this switch's table. ''' api_name = 'http://schema.massopencloud.org/haas/v0/switches/deferred' __mapper_args__ = { 'polymorphic_identity': api_name, } id = db.Column(db.Integer, db.ForeignKey('switch.id'), primary_key=True) hostname = db.Column(db.String, nullable=False) username = db.Column(db.String, nullable=False) password = db.Column(db.String, nullable=False) last_count = None @staticmethod def validate(kwargs): """Implement Switch.validate. This currently doesn't check anything; in theory we should validate that the arguments are sane, but right now aren't worrying about it since this method is only called from the API server, and we never use this switch type there. """ def session(self): """Return a switch session. This just returns self, since there's no connection to speak of. """ return self def disconnect(self): """Implement the session's disconnect() method. This is a no-op, since session() doesn't establish a connection. """ def modify_port(self, port, channel, network_id): """Implement Switch.modify_port. This implementation keeps track of how many pending NetworkingActions there are, and fails the test if apply_networking calls it without committing the previous change. """ # get a new connection to database so that this method does # not see uncommited changes by `apply_networking` local_db = new_db() current_count = local_db.session \ .query(model.NetworkingAction).filter_by(status='PENDING') \ .count() local_db.session.commit() local_db.session.close() if self.last_count is None: self.last_count = current_count else: assert current_count == self.last_count - 1, \ "network daemon did not commit previous change!" self.last_count = current_count def revert_port(self, port): """Implement Switch.revert_port. This always fails, which the tests rely on to check error handling behavior. """ raise RevertPortError('revert_port always fails.')
class Brocade(Switch, SwitchSession): """Brocade switch""" api_name = 'http://schema.massopencloud.org/haas/v0/switches/brocade' __mapper_args__ = { 'polymorphic_identity': api_name, } id = db.Column(BigIntegerType, db.ForeignKey('switch.id'), primary_key=True) hostname = db.Column(db.String, nullable=False) username = db.Column(db.String, nullable=False) password = db.Column(db.String, nullable=False) interface_type = db.Column(db.String, nullable=False) @staticmethod def validate(kwargs): schema.Schema({ 'hostname': basestring, 'username': basestring, 'password': basestring, 'interface_type': basestring, }).validate(kwargs) def session(self): return self @staticmethod def validate_port_name(port): """Valid port names for this switch are of the form 1/0/1 or 1/2""" val = re.compile(r'^\d+/\d+(/\d+)?$') if not val.match(port): raise BadArgumentError("Invalid port name. Valid port names for " "this switch are of the from 1/0/1 or 1/2") return def disconnect(self): pass def modify_port(self, port, channel, new_network): # XXX: We ought to be able to do a Port.query ... one() here, but # there's somthing I(zenhack) don't understand going on with when # things are committed in the tests for this driver, and we don't # get any results that way. We should figure out what's going on with # that test and change this. (port, ) = filter(lambda p: p.label == port, self.ports) interface = port.label if channel == 'vlan/native': if new_network is None: self._remove_native_vlan(interface) else: self._set_native_vlan(interface, new_network) else: match = re.match(re.compile(r'vlan/(\d+)'), channel) assert match is not None, "HIL passed an invalid channel to the" \ " switch!" vlan_id = match.groups()[0] if new_network is None: self._remove_vlan_from_trunk(interface, vlan_id) else: assert new_network == vlan_id self._add_vlan_to_trunk(interface, vlan_id) def revert_port(self, port): self._remove_all_vlans_from_trunk(port) if self._get_native_vlan(port) is not None: self._remove_native_vlan(port) def get_port_networks(self, ports): """Get port configurations of the switch. Args: ports: List of ports to get the configuration for. Returns: Dictionary containing the configuration of the form: { Port<"port-3">: [("vlan/native", "23"), ("vlan/52", "52")], Port<"port-7">: [("vlan/23", "23")], Port<"port-8">: [("vlan/native", "52")], ... } """ response = {} for port in ports: response[port] = filter(None, [self._get_native_vlan(port.label)]) \ + self._get_vlans(port.label) return response def _get_mode(self, interface): """ Return the mode of an interface. Args: interface: interface to return the mode of Returns: 'access' or 'trunk' Raises: AssertionError if mode is invalid. """ url = self._construct_url(interface, suffix='mode') response = self._make_request('GET', url) root = etree.fromstring(response.text) mode = root.find(self._construct_tag('vlan-mode')).text return mode def _enable_and_set_mode(self, interface, mode): """ Enables switching and sets the mode of an interface. Args: interface: interface to set the mode of mode: 'access' or 'trunk' Raises: AssertionError if mode is invalid. """ # Enable switching url = self._construct_url(interface) payload = '<switchport></switchport>' self._make_request('POST', url, data=payload, acceptable_error_codes=(409, )) # Set the interface mode if mode in ['access', 'trunk']: url = self._construct_url(interface, suffix='mode') payload = '<mode><vlan-mode>%s</vlan-mode></mode>' % mode self._make_request('PUT', url, data=payload) else: raise AssertionError('Invalid mode') def _get_vlans(self, interface): """ Return the vlans of a trunk port. Does not include the native vlan. Use _get_native_vlan. Args: interface: interface to return the vlans of Returns: List containing the vlans of the form: [('vlan/vlan1', vlan1), ('vlan/vlan2', vlan2)] """ try: url = self._construct_url(interface, suffix='trunk') response = self._make_request('GET', url) root = etree.fromstring(response.text) vlans = root.\ find(self._construct_tag('allowed')).\ find(self._construct_tag('vlan')).\ find(self._construct_tag('add')).text return [('vlan/%s' % x, x) for x in vlans.split(',')] except AttributeError: return [] def _get_native_vlan(self, interface): """ Return the native vlan of an interface. Args: interface: interface to return the native vlan of Returns: Tuple of the form ('vlan/native', vlan) or None """ try: url = self._construct_url(interface, suffix='trunk') response = self._make_request('GET', url) root = etree.fromstring(response.text) vlan = root.find(self._construct_tag('native-vlan')).text return ('vlan/native', vlan) except AttributeError: return None def _add_vlan_to_trunk(self, interface, vlan): """ Add a vlan to a trunk port. If the port is not trunked, its mode will be set to trunk. Args: interface: interface to add the vlan to vlan: vlan to add """ self._enable_and_set_mode(interface, 'trunk') url = self._construct_url(interface, suffix='trunk/allowed/vlan') payload = '<vlan><add>%s</vlan></vlan>' % vlan self._make_request('PUT', url, data=payload) def _remove_vlan_from_trunk(self, interface, vlan): """ Remove a vlan from a trunk port. Args: interface: interface to remove the vlan from vlan: vlan to remove """ url = self._construct_url(interface, suffix='trunk/allowed/vlan') payload = '<vlan><remove>%s</remove></vlan>' % vlan self._make_request('PUT', url, data=payload) def _remove_all_vlans_from_trunk(self, interface): """ Remove all vlan from a trunk port. Args: interface: interface to remove the vlan from """ url = self._construct_url(interface, suffix='trunk/allowed/vlan') payload = '<vlan><none>true</none></vlan>' requests.put(url, data=payload, auth=self._auth) def _set_native_vlan(self, interface, vlan): """ Set the native vlan of an interface. Args: interface: interface to set the native vlan to vlan: vlan to set as the native vlan """ self._enable_and_set_mode(interface, 'trunk') self._disable_native_tag(interface) url = self._construct_url(interface, suffix='trunk') payload = '<trunk><native-vlan>%s</native-vlan></trunk>' % vlan self._make_request('PUT', url, data=payload) def _remove_native_vlan(self, interface): """ Remove the native vlan from an interface. Args: interface: interface to remove the native vlan from """ url = self._construct_url(interface, suffix='trunk/native-vlan') self._make_request('DELETE', url) def _disable_native_tag(self, interface): """ Disable tagging of the native vlan Args: interface: interface to disable the native vlan tagging of """ url = self._construct_url(interface, suffix='trunk/tag/native-vlan') self._make_request('DELETE', url, acceptable_error_codes=(404, )) def _construct_url(self, interface, suffix=''): """ Construct the API url for a specific interface appending suffix. Args: interface: interface to construct the url for suffix: suffix to append at the end of the url Returns: string with the url for a specific interface and operation """ # %22 is the encoding for double quotes (") in urls. # % escapes the % character. # Double quotes are necessary in the url because switch ports contain # forward slashes (/), ex. 101/0/10 is encoded as "101/0/10". return '%(hostname)s/rest/config/running/interface/' \ '%(interface_type)s/%%22%(interface)s%%22%(suffix)s' \ % { 'hostname': self.hostname, 'interface_type': self.interface_type, 'interface': interface, 'suffix': '/switchport/%s' % suffix if suffix else '' } @property def _auth(self): return self.username, self.password @staticmethod def _construct_tag(name): """ Construct the xml tag by prepending the brocade tag prefix. """ return '{urn:brocade.com:mgmt:brocade-interface}%s' % name def _make_request(self, method, url, data=None, acceptable_error_codes=()): r = requests.request(method, url, data=data, auth=self._auth) if r.status_code >= 400 and \ r.status_code not in acceptable_error_codes: logger.error('Bad Request to switch. Response: %s', r.text) return r
class Ovs(Switch, SwitchSession): """ Driver for openvswitch. """ api_name = 'http://schema.massopencloud.org/haas/v0/switches/ovs' __mapper_args__ = { 'polymorphic_identity': api_name, } id = db.Column(BigIntegerType, db.ForeignKey('switch.id'), primary_key=True) hostname = db.Column(db.String, nullable=False) username = db.Column(db.String, nullable=False) password = db.Column(db.String, nullable=False) @staticmethod def validate(kwargs): """Checks input to match switch parameters.""" schema.Schema({ 'username': basestring, 'hostname': basestring, 'password': basestring, }).validate(kwargs) # 1. Public Methods: def get_capabilities(self): return ['nativeless-trunk-mode'] def ovs_connect(self, *command_strings): """Interacts with the Openvswitch. Args: *command_strings (tuple) : tuple of shell commands required to make changes to openvswitch Raises: SwitchError Returns: If successful returns None else logs error message """ try: for command in command_strings: arg_list = shlex.split(command) subprocess.check_call(arg_list) except subprocess.CalledProcessError as e: logger.error('%s', e) raise SwitchError('ovs command failed: ') def get_port_networks(self, ports): response = {} for port in ports: trunk_id_list = self._interface_info(port.label)['trunks'] if trunk_id_list == []: response[port] = [] else: result = [] for trunk in trunk_id_list: name = "vlan/" + trunk result.append((name, trunk)) response[port] = result native = self._interface_info(port.label)['tag'] if native != []: response[port].append(("vlan/native", native)) return response # (port,) = filter(lambda p: p.label == port, self.ports) # interface = port.label # port_info = self._interface_info(interface) # networks = [] # if port_info['trunks']: # for vlan in port_info['trunks']: # networks.append(vlan) # if port_info['tag']: # networks.append(port_info['tag'][0]) # return networks def revert_port(self, port): shell_cmd_1 = 'sudo ovs-vsctl del-port {port}'.format(port=port) shell_cmd_2 = 'sudo ovs-vsctl add-port {switch} {port}'.format( switch=self.hostname, port=port) + ' vlan_mode=native-untagged' self.ovs_connect(shell_cmd_1, shell_cmd_2) def modify_port(self, port, channel, new_network): (port, ) = filter(lambda p: p.label == port, self.ports) interface = port.label if channel == 'vlan/native': if new_network is None: return self._remove_native_vlan(interface) else: return self._set_native_vlan(interface, new_network) else: match = re.match(re.compile(r'vlan/(\d+)'), channel) assert match is not None, "HIL passed an invalid channel to the" \ " switch!" vlan_id = match.groups()[0] if new_network is None: return self._remove_vlan_from_port(interface, vlan_id) else: assert new_network == vlan_id return self._add_vlan_to_trunk(interface, vlan_id) # 2. Private Methods: def _string_to_list(self, a_string): """Converts a string representation of list to list. Args: a_string: list output recieved as string. e.g. Strings starting with '[' and ending with ']' Returns: object of list type. Empty list is put as None. """ if a_string == '[]': return ast.literal_eval(a_string) else: a_string = a_string.replace("[", "['").replace("]", "']") a_string = a_string.replace(",", "','") a_list = ast.literal_eval(a_string) a_list = [ele.strip() for ele in a_list] return a_list def _string_to_dict(self, a_string): """Converts a string representation of dictionary into a dictionary type object. Args: a_string: dictionary recieved as type string eg. Strings starting with '{' and ending with '}' Returns: Object of dictionary type. """ if a_string == '{}': a_dict = ast.literal_eval(a_string) return a_dict else: a_string = a_string.replace("{", "{'").replace("}", "'}") a_string = a_string.replace(":", "':'").replace(",", "','") a_dict = ast.literal_eval(a_string) a_dict = {k.strip(): v.strip() for k, v in a_dict.iteritems()} return a_dict def _interface_info(self, port): """Gets latest configuration of port from switch. Args: port: Valid port name Returns: configuration status of the port """ # This function is differnet then `ovs_connect` as it uses # subprocess.check_output because it only needs read info from switch # and pass the output to calling funtion. shell_cmd = "sudo ovs-vsctl list port {port}".format(port=port) args = shlex.split(shell_cmd) try: output = subprocess.check_output(args) except subprocess.CalledProcessError as e: logger.error(" %s ", e) raise SwitchError('Ovs command failed: ') output = output.split('\n') output.remove('') i_info = dict(s.split(':') for s in output) i_info = {k.strip(): v.strip() for k, v in i_info.iteritems()} # above statement removes extra white spaces from the keys and values. # That is important since other calls will be querying this dictionary. for x in i_info.keys(): if i_info[x][0] == "{": i_info[x] = self._string_to_dict(i_info[x]) elif i_info[x][0] == "[": i_info[x] = self._string_to_list(i_info[x]) return i_info def _remove_native_vlan(self, port): """Removes native vlan from a trunked port. If it is the last vlan to be removed, it disables the port and reverts its state to default configuration Args: port: Valid switch port Returns: if successful None else error message """ port_info = self._interface_info(port) vlan_id = port_info['tag'] shell_cmd = "sudo ovs-vsctl remove port {port} tag {vlan_id}".format( port=port, vlan_id=vlan_id) return self.ovs_connect(shell_cmd) def _set_native_vlan(self, port, new_network): """Sets native vlan for a trunked port. It enables the port, if it is the first vlan for the port. Args: port: valid port of switch new_network: vlan_id """ shell_cmd = ("sudo ovs-vsctl set port {port} tag={netid}".format( port=port, netid=new_network) + " vlan_mode=native-untagged") return self.ovs_connect(shell_cmd) def _add_vlan_to_trunk(self, port, vlan_id): """ Adds vlans to a trunk port. """ #import pdb; pdb.set_trace() port_info = self._interface_info(port) netLen = len(port_info['trunks']) if len > 0: shell_cmd = "sudo ovs-vsctl set port {port} trunks={vlans}".format( port=port, vlans=vlan_id) else: all_trunks = ','.join(port_info['trunks']) + ',' + vlan_id shell_cmd = "sudo ovs-vsctl set port {port} trunks={vlans}".format( port=port, vlans=all_trunks) return self.ovs_connect(shell_cmd) def _remove_vlan_from_port(self, port, vlan_id): """ removes a single vlan specified by `vlan_id` """ port_info = self._interface_info(port) shell_cmd = "sudo ovs-vsctl remove port {port} trunks {vlan}".format( port=port, vlan=vlan_id) if port_info['trunks']: return self.ovs_connect(shell_cmd) return None # 3. superclass methods (Not Required): @staticmethod def validate_port_name(port): """This driver accepts any string as a port name. """ def session(self): return self def disconnect(self): return self
class DellNOS9(Switch, SwitchSession): """Dell S3048-ON running Dell NOS9""" api_name = 'http://schema.massopencloud.org/haas/v0/switches/dellnos9' __mapper_args__ = { 'polymorphic_identity': api_name, } id = db.Column(BigIntegerType, db.ForeignKey('switch.id'), primary_key=True) hostname = db.Column(db.String, nullable=False) username = db.Column(db.String, nullable=False) password = db.Column(db.String, nullable=False) interface_type = db.Column(db.String, nullable=False) @staticmethod def validate(kwargs): Schema({ 'hostname': basestring, 'username': basestring, 'password': basestring, 'interface_type': basestring, }).validate(kwargs) def session(self): return self def ensure_legal_operation(self, nic, op_type, channel): check_native_networks(nic, op_type, channel) def get_capabilities(self): return [] @staticmethod def validate_port_name(port): """Valid port names for this switch are of the form 1/0/1 or 1/2""" if not re.match(r'^\d+/\d+(/\d+)?$', port): raise BadArgumentError("Invalid port name. Valid port names for " "this switch are of the form 1/0/1 or 1/2") return def disconnect(self): """Since the switch is not connection oriented, we don't need to establish a session or disconnect from it.""" def modify_port(self, port, channel, new_network): (port, ) = filter(lambda p: p.label == port, self.ports) interface = port.label if channel == 'vlan/native': if new_network is None: self._remove_native_vlan(interface) self._port_shutdown(interface) else: self._set_native_vlan(interface, new_network) else: vlan_id = channel.replace('vlan/', '') legal = get_network_allocator(). \ is_legal_channel_for(channel, vlan_id) assert legal, "HIL passed an invalid channel to the switch!" if new_network is None: self._remove_vlan_from_trunk(interface, vlan_id) else: assert new_network == vlan_id self._add_vlan_to_trunk(interface, vlan_id) if should_save(self): self.save_running_config() def revert_port(self, port): self._remove_all_vlans_from_trunk(port) if self._get_native_vlan(port) is not None: self._remove_native_vlan(port) self._port_shutdown(port) if should_save(self): self.save_running_config() def get_port_networks(self, ports): response = {} for port in ports: response[port] = self._get_vlans(port.label) native = self._get_native_vlan(port.label) if native is not None: response[port].append(native) return response def _get_vlans(self, interface): """ Return the vlans of a trunk port. Does not include the native vlan. Use _get_native_vlan. Args: interface: interface to return the vlans of Returns: List containing the vlans of the form: [('vlan/vlan1', vlan1), ('vlan/vlan2', vlan2)] """ # It uses the REST API CLI which is slow but it is the only way # because the switch is VLAN centric. Doing a GET on interface won't # return the VLANs on it, we would have to do get on all vlans (if that # worked reliably in the first place) and then find our interface there # which is not feasible. if not self._is_port_on(interface): return [] response = self._get_port_info(interface) # finds a comma separated list of integers and/or ranges starting with # T. Sample T12,14-18,23,28,80-90 or T20 or T20,22 or T20-22 match = re.search(r'T(\d+(-\d+)?)(,\d+(-\d+)?)*', response) if match is None: return [] vlan_list = parse_vlans(match.group().replace('T', '')) return [('vlan/%s' % x, x) for x in vlan_list] def _get_native_vlan(self, interface): """ Return the native vlan of an interface. Args: interface: interface to return the native vlan of Returns: Tuple of the form ('vlan/native', vlan) or None Similar to _get_vlans() """ if not self._is_port_on(interface): return None response = self._get_port_info(interface) match = re.search(r'NativeVlanId:(\d+)\.', response) if match is not None: vlan = match.group(1) else: logger.error('Unexpected: No native vlan found') return return ('vlan/native', vlan) def _get_port_info(self, interface): """Returns the output of a show interface command. This removes all spaces from the response before returning it which is then parsed by the caller. Sample Response: u"<outputxmlns='http://www.dell.com/ns/dell:0.1/root'>\n <command>show interfaces switchport GigabitEthernet1/3\r\n\r\n Codes: U-Untagged T-Tagged\r\n x-Dot1x untagged,X-Dot1xtagged\r\n G-GVRP tagged,M-Trunk\r\n i-Internal untagged, I-Internaltagged, v-VLTuntagged, V-VLTtagged\r\n\r\n Name:GigabitEthernet1/3\r\n 802.1Q Tagged:Hybrid\r\n Vlan membership:\r\n Q Vlans\r\n U 1512 \r\n T 1511 1612-1614,1700\r\n\r\n Native Vlan Id: 1512.\r\n\r\n\r\n\r\n MOC-Dell-S3048-ON#</command>\n</output>\n" """ command = 'interfaces switchport %s %s' % \ (self.interface_type, interface) response = self._execute(SHOW, command) return response.text.replace(' ', '') def _add_vlan_to_trunk(self, interface, vlan): """ Add a vlan to a trunk port. If the port is not trunked, its mode will be set to trunk. Args: interface: interface to add the vlan to vlan: vlan to add """ if not self._is_port_on(interface): self._port_on(interface) command = 'interface vlan ' + vlan + '\r\n tagged ' + \ self.interface_type + ' ' + interface self._execute(CONFIG, command) def _remove_vlan_from_trunk(self, interface, vlan): """ Remove a vlan from a trunk port. Args: interface: interface to remove the vlan from vlan: vlan to remove """ command = self._remove_vlan_command(interface, vlan) self._execute(CONFIG, command) def _remove_all_vlans_from_trunk(self, interface): """ Remove all vlan from a trunk port. Args: interface: interface to remove the vlan from """ command = '' for vlan in self._get_vlans(interface): command += self._remove_vlan_command(interface, vlan[1]) + '\r\n ' # execute command only if there are some vlans to remove, otherwise # the switch complains if command is not '': self._execute(CONFIG, command) def _remove_vlan_command(self, interface, vlan): """Returns command to remove <vlan> from <interface>""" return 'interface vlan ' + vlan + '\r\n no tagged ' + \ self.interface_type + ' ' + interface def _set_native_vlan(self, interface, vlan): """ Set the native vlan of an interface. Args: interface: interface to set the native vlan to vlan: vlan to set as the native vlan Method relies on the REST API CLI which is slow """ if not self._is_port_on(interface): self._port_on(interface) command = 'interface vlan ' + vlan + '\r\n untagged ' + \ self.interface_type + ' ' + interface self._execute(CONFIG, command) def _remove_native_vlan(self, interface): """ Remove the native vlan from an interface. Args: interface: interface to remove the native vlan from.vlan """ try: vlan = self._get_native_vlan(interface)[1] command = 'interface vlan ' + vlan + '\r\n no untagged ' + \ self.interface_type + ' ' + interface self._execute(CONFIG, command) except TypeError: logger.error('No native vlan to remove') def _port_shutdown(self, interface): """ Shuts down <interface> Turn off portmode hybrid, disable switchport, and then shut down the port. All non-default vlans must be removed before calling this. """ url = self._construct_url(interface=interface) interface = self._convert_interface_type(self.interface_type) + \ interface.replace('/', '-') payload = '<interface><name>%s</name><portmode><hybrid>false' \ '</hybrid></portmode><shutdown>true</shutdown>' \ '</interface>' % interface self._make_request('PUT', url, data=payload) def _port_on(self, interface): """ Turns on <interface> Turn on port and enable hybrid portmode and switchport. """ url = self._construct_url(interface=interface) interface = self._convert_interface_type(self.interface_type) + \ interface.replace('/', '-') payload = '<interface><name>%s</name><portmode><hybrid>true' \ '</hybrid></portmode><switchport></switchport>' \ '<shutdown>false</shutdown></interface>' % interface self._make_request('PUT', url, data=payload) def _is_port_on(self, port): """ Returns a boolean that tells the status of a switchport""" # the url here requires a suffix to GET the shutdown tag in response. url = self._construct_url(interface=port) + r'\?with-defaults' response = self._make_request('GET', url) root = etree.fromstring(response.text) shutdown = root.find(self._construct_tag('shutdown')).text assert shutdown in ('false', 'true'), "unexpected state of switchport" return shutdown == 'false' def save_running_config(self): command = 'write' self._execute(EXEC, command) def get_config(self, config_type): command = config_type + '-config' config = self._execute(SHOW, command).text # The config files always have some lines in the beginning that we # need to remove otherwise the comparison would fail. Here's a sample: # Current Configuration ... # ! Version 9.11(0.0P6) # ! Last configuration change at Fri Nov 3 23:51:01 2017 by smartuser # ! Startup-config last updated at Sat Nov 4 02:04:57 2017 by admin # ! # boot system stack-unit 1 primary system://A # boot system stack-unit 1 secondary system://B # ! # hostname MOC-Dell-S3048-ON # ! # protocol lldp # ! # redundancy auto-synchronize full # ! # username xxxxx password 7 XXXXXXXx privilege 15 # ! # stack-unit 1 provision S3048-ON lines_to_remove = 0 lines = config.splitlines() for line in lines: if 'username' in line: break lines_to_remove += 1 config = '\n'.join(lines[lines_to_remove:]) # there were some extra spaces in one of the config file types that # would cause the tests to fail. return config.replace(" ", "") # HELPER METHODS ********************************************* def _execute(self, command_type, command): """This method gets the url & the payload and executes <command>""" url = self._construct_url() payload = self._make_payload(command_type, command) return self._make_request('POST', url, data=payload) def _construct_url(self, interface=None): """ Construct the API url for a specific interface. Args: interface: interface to construct the url for Returns: string with the url for a specific interface and operation If interface is None, then it returns the URL for REST API CLI. """ if interface is None: return '%s/api/running/dell/_operations/cli' % self.hostname try: self.validate_port_name(interface) # if `interface` refers to port name # the urls have dashes instead of slashes in interface names interface = interface.replace('/', '-') interface_type = self._convert_interface_type(self.interface_type) except BadArgumentError: # interface refers to `vlan` interface_type = 'vlan-' return ''.join([ self.hostname, '/api/running/dell/interfaces/' 'interface/', interface_type, interface ]) @staticmethod def _convert_interface_type(interface_type): """ Convert the interface type from switch CLI form to what the API server understands. Args: interface: the interface in the CLI-form Returns: string interface """ iftypes = { 'GigabitEthernet': 'gige-', 'TenGigabitEthernet': 'tengig-', 'TwentyfiveGigabitEthernet': 'twentyfivegig-', 'fortyGigE': 'fortygig-', 'peGigabitEthernet': 'pegig-', 'FiftyGigabitEthernet': 'fiftygig-', 'HundredGigabitEthernet': 'hundredgig-' } return iftypes[interface_type] @property def _auth(self): return self.username, self.password @staticmethod def _make_payload(command_type, command): """Makes payload for passing CLI commands using the REST API""" return '<input><%s>%s</%s></input>' % (command_type, command, command_type) @staticmethod def _construct_tag(name): """ Construct the xml tag by prepending the dell tag prefix. """ return '{http://www.dell.com/ns/dell:0.1/root}%s' % name def _make_request(self, method, url, data=None): r = requests.request(method, url, data=data, auth=self._auth) if r.status_code >= 400: logger.error('Bad Request to switch. Response: %s', r.text) return r
class Ovs(Switch, SwitchSession): """ Driver for openvswitch. """ api_name = 'http://schema.massopencloud.org/haas/v0/switches/ovs' __mapper_args__ = { 'polymorphic_identity': api_name, } id = db.Column( BigIntegerType, db.ForeignKey('switch.id'), primary_key=True ) ovs_bridge = db.Column(db.String, nullable=False) @staticmethod def validate(kwargs): """Checks input to match switch parameters.""" schema.Schema({ 'ovs_bridge': basestring, }).validate(kwargs) # 1. Public Methods: def get_capabilities(self): return ['nativeless-trunk-mode'] def ovs_connect(self, *command_strings): """Interacts with the Openvswitch. Args: *command_strings (tuple) : tuple of list of arguments required to make changes to openvswitch Raises: SwitchError Returns: If successful returns None else logs error message """ try: for arg_list in command_strings: subprocess.check_call(arg_list) except subprocess.CalledProcessError as e: logger.error('%s', e) raise SwitchError('ovs command failed: %s', e) def get_port_networks(self, ports): response = {} for port in ports: trunk_id_list = self._interface_info(port.label)['trunks'] if trunk_id_list == []: response[port] = [] else: result = [] for trunk in trunk_id_list: name = "vlan/"+trunk result.append((name, trunk)) response[port] = result native = self._interface_info(port.label)['tag'] if native != []: response[port].append(("vlan/native", native)) return response def revert_port(self, port): args_1 = ['sudo', 'ovs-vsctl', 'del-port', str(port)] args_2 = [ 'sudo', 'ovs-vsctl', 'add-port', str(self.ovs_bridge), str(port), 'vlan_mode=native-untagged' ] self.ovs_connect(args_1, args_2) def modify_port(self, port, channel, new_network): port_obj = Port.query.filter_by(label=port).one() interface = port_obj.label if channel == 'vlan/native': if new_network is None: return self._remove_native_vlan(interface) else: return self._set_native_vlan(interface, new_network) else: match = re.match(re.compile(r'vlan/(\d+)'), channel) assert match is not None, "HIL passed an invalid channel to the" \ " switch!" vlan_id = match.groups()[0] if new_network is None: return self._remove_vlan_from_port(interface, vlan_id) else: assert new_network == vlan_id return self._add_vlan_to_trunk(interface, vlan_id) def _interface_info(self, port): """Gets latest configuration of port from switch. Args: port: Valid port name Returns intermediate output which processed to return final output: It is a string of the following format: '_uuid : ad489368-9b53-4a3e-8732-697ad5141de9\n bond_downdelay : 0\nbon d_fake_iface : false\n external_ids : {}\nfake_bridge : false\n interfaces : [fc61c8ff99c5,fc61c8ff99c6]\n name : "veth-0"\n statistics : {abc:123 , def:xyz, space : lot of it , \ 2345:some number}\n status : {}\ntag : 100\n trunks : [200,300,400]\n vlan_mode : native-untagged\n' Which is used as input for further processing. Returns: A dictionary of configuration status of the port. String, Dictionary and list are valid values of this dictionary. eg: Sample output. { '_uuid': 'ad489368-9b53-4a3e-8732-697ad5141de9', 'bond_downdelay': '0', 'bond_fake_iface': 'false', 'external_ids': {}, 'fake_bridge': 'false', 'interfaces': ['fc61c8ff99c5', 'fc61c8ff99c6'], 'name': '"veth-0"', 'statistics': {'2345': 'some number','abc': '123','def': 'xyz' ,'space': 'lot of it'}, 'status': {}, 'tag': '100', 'trunks': ['200', '300', '400'], 'vlan_mode': 'native-untagged' } """ # This function is differnet then `ovs_connect` as it uses # subprocess.check_output because it only needs read info from switch # and pass the output to calling funtion. args = ['sudo', 'ovs-vsctl', 'list', 'port', str(port)] try: output = subprocess.check_output(args) except subprocess.CalledProcessError as e: logger.error(" %s ", e) raise SwitchError('Ovs command failed: %s', e) output = output.split('\n') output.remove('') i_info = dict(s.split(':', 1) for s in output) i_info = {k.strip(): v.strip() for k, v in i_info.iteritems()} for x in i_info.keys(): if i_info[x][0] == "{": i_info[x] = string_to_dict(i_info[x]) elif i_info[x][0] == "[": i_info[x] = string_to_list(i_info[x]) return i_info def _remove_native_vlan(self, port): """Removes native vlan from a trunked port. If it is the last vlan to be removed, it disables the port and reverts its state to default configuration Args: port: Valid switch port Returns: if successful None else error message """ port_info = self._interface_info(port) vlan_id = port_info['tag'] args = [ 'sudo', 'ovs-vsctl', 'remove', 'port', str(port), 'tag', str(vlan_id) ] return self.ovs_connect(args) def _set_native_vlan(self, port, new_network): """Sets native vlan for a trunked port. It enables the port, if it is the first vlan for the port. Args: port: valid port of switch new_network: vlan_id """ args = [ 'sudo', 'ovs-vsctl', 'set', 'port', str(port), 'tag='+str(new_network), 'vlan_mode=native-untagged' ] return self.ovs_connect(args) def _add_vlan_to_trunk(self, port, vlan_id): """ Adds vlans to a trunk port. """ port_info = self._interface_info(port) if not port_info['trunks']: args = [ 'sudo', 'ovs-vsctl', 'set', 'port', str(port), 'trunks='+str(vlan_id) ] else: all_trunks = ','.join(port_info['trunks'])+','+vlan_id args = [ 'sudo', 'ovs-vsctl', 'set', 'port', str(port), 'trunks='+str(all_trunks) ] return self.ovs_connect(args) def _remove_vlan_from_port(self, port, vlan_id): """ removes a single vlan specified by `vlan_id` """ port_info = self._interface_info(port) args = [ 'sudo', 'ovs-vsctl', 'remove', 'port', str(port), 'trunks', str(vlan_id) ] if port_info['trunks']: return self.ovs_connect(args) return None # 3. Other superclass methods: @staticmethod def validate_port_name(port): """This driver accepts any string as a port name. """ def session(self): return self def disconnect(self): pass
class Ipmi(Obm): valid_bootdevices = ['disk', 'pxe', 'none'] id = db.Column(BigIntegerType, db.ForeignKey('obm.id'), primary_key=True) host = db.Column(db.String, nullable=False) user = db.Column(db.String, nullable=False) password = db.Column(db.String, nullable=False) api_name = 'http://schema.massopencloud.org/haas/v0/obm/ipmi' __mapper_args__ = { 'polymorphic_identity': api_name, } @staticmethod def validate(kwargs): schema.Schema({ 'type': Ipmi.api_name, 'host': basestring, 'user': basestring, 'password': basestring, }).validate(kwargs) def _ipmitool(self, args): """Invoke ipmitool with the right host/pass etc. for this node. `args`- A list of any additional arguments to pass to ipmitool. Returns the exit status of ipmitool. Note: Includes the ``-I lanplus`` flag, available only in IPMI v2+. This is needed for machines which do not accept the older version. """ status = call([ 'ipmitool', '-I', 'lanplus', # see docstring above '-U', self.user, '-P', self.password, '-H', self.host ] + args) if status != 0: logger = logging.getLogger(__name__) logger.info('Nonzero exit status form ipmitool, args = %r', args) return status @no_dry_run def power_cycle(self, force): self._ipmitool(['chassis', 'bootdev', 'pxe']) if force: op = 'reset' else: op = 'cycle' if self._ipmitool(['chassis', 'power', op]) == 0: return if self._ipmitool(['chassis', 'power', 'on']) == 0: # power cycle will fail if the machine is not running. # To avoid such a situation, just turn it on anyways. # Doing this saves power by turning things off without # Without breaking the HIL. return # If it is still does not work, then it is a real error: raise OBMError('Could not power cycle node %s' % self.node.label) @no_dry_run def power_off(self): if self._ipmitool(['chassis', 'power', 'off']) != 0: raise OBMError('Could not power off node %s', self.label) def require_legal_bootdev(self, dev): if dev not in self.valid_bootdevices: raise BadArgumentError('Invald boot device') @no_dry_run def set_bootdev(self, dev): self.require_legal_bootdev(dev) if self._ipmitool(['chassis', 'bootdev', dev, 'options=persistent' ]) != 0: raise OBMError('Could not set boot device') @no_dry_run def start_console(self): """Starts logging the IPMI console.""" # stdin and stderr are redirected to a PIPE that is never read in order # to prevent stdout from becoming garbled. This happens because # ipmitool sets shell settings to behave like a tty when communicateing # over Serial over Lan Popen([ 'ipmitool', '-H', self.host, '-U', self.user, '-P', self.password, '-I', 'lanplus', 'sol', 'activate' ], stdin=PIPE, stdout=open(self.get_console_log_filename(), 'a'), stderr=PIPE) # stdin, stdout, and stderr are redirected to a pipe that is never read # because we are not interested in the ouput of this command. @no_dry_run def stop_console(self): call(['pkill', '-f', 'ipmitool -H %s' % self.host]) proc = Popen([ 'ipmitool', '-H', self.host, '-U', self.user, '-P', self.password, '-I', 'lanplus', 'sol', 'deactivate' ], stdin=PIPE, stdout=PIPE, stderr=PIPE) proc.wait() def delete_console(self): if os.path.isfile(self.get_console_log_filename()): os.remove(self.get_console_log_filename()) def get_console(self): if not os.path.isfile(self.get_console_log_filename()): return None with open(self.get_console_log_filename(), 'r') as log: return "".join(i for i in log.read() if ord(i) < 128) def get_console_log_filename(self): return '/var/run/hil_console_logs/%s.log' % self.host
class Brocade(Switch, _vlan_http.Session): """Brocade switch""" api_name = 'http://schema.massopencloud.org/haas/v0/switches/brocade' __mapper_args__ = { 'polymorphic_identity': api_name, } id = db.Column(BigIntegerType, db.ForeignKey('switch.id'), primary_key=True) hostname = db.Column(db.String, nullable=False) username = db.Column(db.String, nullable=False) password = db.Column(db.String, nullable=False) interface_type = db.Column(db.String, nullable=False) @staticmethod def validate(kwargs): Schema({ 'hostname': basestring, 'username': basestring, 'password': basestring, 'interface_type': basestring, }).validate(kwargs) def session(self): return self def ensure_legal_operation(self, nic, op_type, channel): check_native_networks(nic, op_type, channel) @staticmethod def validate_port_name(port): """Valid port names for this switch are of the form 1/0/1 or 1/2""" val = re.compile(r'^\d+/\d+(/\d+)?$') if not val.match(port): raise BadArgumentError("Invalid port name. Valid port names for " "this switch are of the from 1/0/1 or 1/2") return def get_capabilities(self): return [] def _port_shutdown(self, interface): """Shuts down port; unimplemented right now TODO: Implement this and associated methods see #970""" def _get_mode(self, interface): """ Return the mode of an interface. Args: interface: interface to return the mode of Returns: 'access' or 'trunk' Raises: AssertionError if mode is invalid. """ url = self._construct_url(interface, suffix='mode') response = self._make_request('GET', url) root = etree.fromstring(response.text) mode = root.find(self._construct_tag('vlan-mode')).text return mode def _enable_and_set_mode(self, interface, mode): """ Enables switching and sets the mode of an interface. Args: interface: interface to set the mode of mode: 'access' or 'trunk' Raises: AssertionError if mode is invalid. """ # Enable switching url = self._construct_url(interface) payload = '<switchport></switchport>' self._make_request('POST', url, data=payload, acceptable_error_codes=(409, )) # Set the interface mode if mode in ['access', 'trunk']: url = self._construct_url(interface, suffix='mode') payload = '<mode><vlan-mode>%s</vlan-mode></mode>' % mode self._make_request('PUT', url, data=payload) else: raise AssertionError('Invalid mode') def _get_vlans(self, interface): """ Return the vlans of a trunk port. Does not include the native vlan. Use _get_native_vlan. Args: interface: interface to return the vlans of Returns: List containing the vlans of the form: [('vlan/vlan1', vlan1), ('vlan/vlan2', vlan2)] """ try: url = self._construct_url(interface, suffix='trunk') response = self._make_request('GET', url) root = etree.fromstring(response.text) vlans = root. \ find(self._construct_tag('allowed')).\ find(self._construct_tag('vlan')).\ find(self._construct_tag('add')).text # finds a comma separated list of integers and/or ranges. # Sample: 12,14-18,23,28,80-90 or 20 or 20,22 or 20-22 match = re.search(r'(\d+(-\d+)?)(,\d+(-\d+)?)*', vlans) if match is None: return [] vlan_list = parse_vlans(match.group()) return [('vlan/%s' % x, x) for x in vlan_list] except AttributeError: return [] def _get_native_vlan(self, interface): """ Return the native vlan of an interface. Args: interface: interface to return the native vlan of Returns: Tuple of the form ('vlan/native', vlan) or None """ try: url = self._construct_url(interface, suffix='trunk') response = self._make_request('GET', url) root = etree.fromstring(response.text) vlan = root.find(self._construct_tag('native-vlan')).text return ('vlan/native', vlan) except AttributeError: return None def _add_vlan_to_trunk(self, interface, vlan): """ Add a vlan to a trunk port. If the port is not trunked, its mode will be set to trunk. Args: interface: interface to add the vlan to vlan: vlan to add """ self._enable_and_set_mode(interface, 'trunk') url = self._construct_url(interface, suffix='trunk/allowed/vlan') payload = '<vlan><add>%s</vlan></vlan>' % vlan self._make_request('PUT', url, data=payload) def _remove_vlan_from_trunk(self, interface, vlan): """ Remove a vlan from a trunk port. Args: interface: interface to remove the vlan from vlan: vlan to remove """ url = self._construct_url(interface, suffix='trunk/allowed/vlan') payload = '<vlan><remove>%s</remove></vlan>' % vlan self._make_request('PUT', url, data=payload) def _remove_all_vlans_from_trunk(self, interface): """ Remove all vlan from a trunk port. Args: interface: interface to remove the vlan from """ url = self._construct_url(interface, suffix='trunk/allowed/vlan') payload = '<vlan><none>true</none></vlan>' requests.put(url, data=payload, auth=self._auth) def _set_native_vlan(self, interface, vlan): """ Set the native vlan of an interface. Args: interface: interface to set the native vlan to vlan: vlan to set as the native vlan """ self._enable_and_set_mode(interface, 'trunk') self._disable_native_tag(interface) url = self._construct_url(interface, suffix='trunk') payload = '<trunk><native-vlan>%s</native-vlan></trunk>' % vlan self._make_request('PUT', url, data=payload) def _remove_native_vlan(self, interface): """ Remove the native vlan from an interface. Args: interface: interface to remove the native vlan from """ url = self._construct_url(interface, suffix='trunk/native-vlan') self._make_request('DELETE', url) def _disable_native_tag(self, interface): """ Disable tagging of the native vlan Args: interface: interface to disable the native vlan tagging of """ url = self._construct_url(interface, suffix='trunk/tag/native-vlan') self._make_request('DELETE', url, acceptable_error_codes=(404, )) def _construct_url(self, interface, suffix=''): """ Construct the API url for a specific interface appending suffix. Args: interface: interface to construct the url for suffix: suffix to append at the end of the url Returns: string with the url for a specific interface and operation """ # %22 is the encoding for double quotes (") in urls. # % escapes the % character. # Double quotes are necessary in the url because switch ports contain # forward slashes (/), ex. 101/0/10 is encoded as "101/0/10". return '%(hostname)s/rest/config/running/interface/' \ '%(interface_type)s/%%22%(interface)s%%22%(suffix)s' \ % { 'hostname': self.hostname, 'interface_type': self.interface_type, 'interface': interface, 'suffix': '/switchport/%s' % suffix if suffix else '' } @staticmethod def _construct_tag(name): """ Construct the xml tag by prepending the brocade tag prefix. """ return '{urn:brocade.com:mgmt:brocade-interface}%s' % name