# (c) Copyright 2015 Hewlett Packard Enterprise Development LP # Copyright 2015 Universidade Federal de Campina Grande # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import abc import json import six import time import requests import retrying from oneview_client import auditing from oneview_client import exceptions from oneview_client import ilo_utils from oneview_client import managers from oneview_client import states from oneview_client import utils SUPPORTED_ONEVIEW_VERSION = 200 WAIT_DO_REQUEST_IN_MILLISECONDS = 1000 WAIT_TASK_IN_MILLISECONDS = 1000 GET_REQUEST_TYPE = 'GET' PUT_REQUEST_TYPE = 'PUT' POST_REQUEST_TYPE = 'POST' DELETE_REQUEST_TYPE = 'DELETE' MOMENTARY_PRESS = 'MomentaryPress' PRESS_AND_HOLD = 'PressAndHold' SERVER_HARDWARE_PREFIX_URI = '/rest/server-hardware/' SERVER_PROFILE_TEMPLATE_PREFIX_URI = '/rest/server-profile-templates/' SERVER_PROFILE_PREFIX_URI = '/rest/server-profiles/' @six.add_metaclass(abc.ABCMeta) class BaseClient(object): def __init__( self, manager_url, username, password, allow_insecure_connections=False, tls_cacert_file='', max_polling_attempts=20, audit_enabled=False, audit_map_file='', audit_output_file='' ): self.manager_url = manager_url self.username = username self.password = password self.allow_insecure_connections = allow_insecure_connections self.tls_cacert_file = tls_cacert_file self.max_polling_attempts = max_polling_attempts self.audit_enabled = audit_enabled self.audit_map_file = audit_map_file self.audit_output_file = audit_output_file self.audit_case_methods = [] if self.allow_insecure_connections: requests.packages.urllib3.disable_warnings( requests.packages.urllib3.exceptions.InsecureRequestWarning ) if self.audit_enabled: self.audit_case_methods = auditing.read_audit_map_file( self.audit_map_file ) self.session_id = self.get_session() @auditing.audit def verify_credentials(self): return self._authenticate() @auditing.audit def get_session(self): response = self._authenticate() return response.json().get('sessionID') @auditing.audit def _authenticate(self): if self.manager_url in ("", None): raise exceptions.OneViewConnectionError( "Can't connect to OneView: 'manager_url' configuration" "parameter is blank") url = '%s/rest/login-sessions' % self.manager_url body = { 'userName': self.username, 'password': self.password } headers = { 'content-type': 'application/json' } verify_ssl = self._get_verify_connection_option() r = requests.post(url, data=json.dumps(body), headers=headers, verify=verify_ssl) if r.status_code == 400: raise exceptions.OneViewNotAuthorizedException() else: return r @auditing.audit def _logout(self): if self.manager_url in ("", None): raise exceptions.OneViewConnectionError( "Can't connect to OneView: 'manager_url' configuration" "parameter is blank") url = '%s/rest/login-sessions' % self.manager_url headers = { 'X-Api-Version': str(SUPPORTED_ONEVIEW_VERSION), 'Auth': self.session_id } verify_ssl = self._get_verify_connection_option() r = requests.delete(url, headers=headers, verify=verify_ssl) if r.status_code == 400: raise exceptions.OneViewNotAuthorizedException() @auditing.audit def _get_verify_connection_option(self): verify_status = False user_cacert = self.tls_cacert_file if self.allow_insecure_connections is False: if not user_cacert: verify_status = True else: verify_status = user_cacert return verify_status @auditing.audit def verify_oneview_version(self): if not self._is_oneview_version_compatible(): msg = ("The version of the OneView's API is unsupported. " "Supported version is '%s'" % SUPPORTED_ONEVIEW_VERSION) raise exceptions.IncompatibleOneViewAPIVersion(msg) @auditing.audit def _is_oneview_version_compatible(self): versions = self.get_oneview_version() v = SUPPORTED_ONEVIEW_VERSION min_version_compatible = versions.get("minimumVersion") <= v max_version_compatible = versions.get("currentVersion") >= v return min_version_compatible and max_version_compatible @auditing.audit def get_oneview_version(self): url = '%s/rest/version' % self.manager_url headers = {"Accept-Language": "en_US"} verify_ssl = self._get_verify_connection_option() response = requests.get( url, headers=headers, verify=verify_ssl ) self._check_request_status(response, url) versions = response.json() return versions # --- Requests --- def _prepare_and_do_request( self, uri, body={}, request_type=GET_REQUEST_TYPE ): json_response = {} try: if not self.session_id: self.session_id = self.get_session() headers = { 'content-type': 'application/json', 'X-Api-Version': str(SUPPORTED_ONEVIEW_VERSION), 'Auth': self.session_id } url = '%s%s' % (self.manager_url, uri) body = json.dumps(body) try: response = self._do_request(url, headers, body, request_type) except exceptions.OneViewNotAuthorizedException: self.session_id = self.get_session() headers['Auth'] = self.session_id response = self._do_request(url, headers, body, request_type) json_response = response.json() except requests.RequestException as e: connection_error = str(e.message).split(':')[-1] log_message = ("Can't connect to OneView: %s" % connection_error) raise exceptions.OneViewConnectionError(log_message) return json_response @auditing.audit def _do_request(self, url, headers, body, request_type): verify_status = self._get_verify_connection_option() @retrying.retry( stop_max_attempt_number=self.max_polling_attempts, retry_on_result=lambda response: self._check_request_status( response, url ), wait_fixed=WAIT_DO_REQUEST_IN_MILLISECONDS ) def request(url, headers, body, request_type): if request_type == PUT_REQUEST_TYPE: response = requests.put( url, data=body, headers=headers, verify=verify_status ) elif request_type == POST_REQUEST_TYPE: response = requests.post( url, data=body, headers=headers, verify=verify_status ) elif request_type == DELETE_REQUEST_TYPE: response = requests.delete( url, headers=headers, verify=verify_status ) else: response = requests.get( url, headers=headers, verify=verify_status ) return response return request(url, headers, body, request_type) @auditing.audit def _wait_for_task_to_complete(self, task): @retrying.retry( retry_on_result=lambda task: task.get('percentComplete') < 100, wait_fixed=WAIT_TASK_IN_MILLISECONDS, retry_on_exception=lambda task: False ) def wait(task): uri = task.get('uri') task = self._prepare_and_do_request(uri) task_state = task.get("taskState") error_code = task.get("errorCode") if (not task_state) and error_code: details = task.get("details") if error_code == "RESOURCE_NOT_FOUND": raise exceptions.OneViewResourceNotFoundError(details) else: raise exceptions.OneViewTaskError("%s - %s" % (error_code, details)) elif task_state.lower() == 'error': raise exceptions.OneViewTaskError("The task '%s' returned an " "error state" % uri) return task return wait(task) @auditing.audit def _get_ilo_access(self, server_hardware_uuid): uri = ("/rest/server-hardware/%s/remoteConsoleUrl" % server_hardware_uuid) json = self._prepare_and_do_request(uri) url = json.get("remoteConsoleUrl") ip_key = "addr=" host_ip = url[url.rfind(ip_key) + len(ip_key):] host_ip = host_ip[:host_ip.find("&")] session_key = "sessionkey=" token = url[url.rfind(session_key) + len(session_key):] return host_ip, token @auditing.audit def get_sh_mac_from_ilo(self, server_hardware_uuid, nic_index=0): host_ip, ilo_token = self._get_ilo_access(server_hardware_uuid) try: return ilo_utils.get_mac_from_ilo(host_ip, ilo_token, nic_index, self.allow_insecure_connections) finally: ilo_utils.ilo_logout(host_ip, ilo_token) @auditing.audit def _set_onetime_boot(self, server_hardware_uuid, boot_device): host_ip, ilo_token = self._get_ilo_access(server_hardware_uuid) oneview_ilo_mapping = { 'HardDisk': 'Hdd', 'PXE': 'Pxe', 'CD': 'Cd', 'USB': 'Usb', } try: ilo_device = oneview_ilo_mapping[boot_device] return ilo_utils.set_onetime_boot(host_ip, ilo_token, ilo_device, self.allow_insecure_connections) except exceptions.IloException as e: raise e except KeyError as e: raise exceptions.IloException( "Set one-time boot to %s is not supported." % boot_device ) finally: ilo_utils.ilo_logout(host_ip, ilo_token) def _check_request_status(self, response, url): repeat = False status = response.status_code if status in (401, 403): error_code = response.json().get('errorCode') raise exceptions.OneViewNotAuthorizedException(error_code) elif status == 404: raise exceptions.OneViewResourceNotFoundError(url) elif status in (408, 409,): time.sleep(10) repeat = True elif status == 500: raise exceptions.OneViewInternalServerError() # Any other unexpected status are logged elif status not in (200, 202,): message = ( "OneView appliance returned an unknown response status: %s" % status ) raise exceptions.UnknowOneViewResponseError(message) return repeat class ClientV2(BaseClient): def __init__( self, manager_url, username, password, allow_insecure_connections=False, tls_cacert_file='', max_polling_attempts=20, audit_enabled=False, audit_map_file='', audit_output_file='' ): super(ClientV2, self).__init__(manager_url, username, password, allow_insecure_connections, tls_cacert_file, max_polling_attempts, audit_enabled, audit_map_file, audit_output_file) # Next generation self.enclosure = managers.EnclosureManager(self) self.enclosure_group = managers.EnclosureGroupManager(self) self.server_hardware = managers.ServerHardwareManager(self) self.server_hardware_index = managers.ServerHardwareIndexManager(self) self.server_hardware_type = managers.ServerHardwareTypeManager(self) self.server_profile = managers.ServerProfileManager(self) self.server_profile_template = managers.ServerProfileTemplateManager( self ) class Client(BaseClient): def __init__( self, manager_url, username, password, allow_insecure_connections=False, tls_cacert_file='', max_polling_attempts=20, audit_enabled=False, audit_map_file='', audit_output_file='' ): super(Client, self).__init__(manager_url, username, password, allow_insecure_connections, tls_cacert_file, max_polling_attempts, audit_enabled, audit_map_file, audit_output_file) # Next generation self._enclosure_group = managers.EnclosureGroupManager(self) self._server_hardware = managers.ServerHardwareManager(self) self._server_profile_template = managers.ServerProfileTemplateManager( self ) self._server_profile = managers.ServerProfileManager(self) # --- Power Driver --- @auditing.audit def get_node_power_state(self, node_info): return self.get_server_hardware(node_info).power_state @auditing.audit def power_on(self, node_info): if self.get_node_power_state(node_info) == states.ONEVIEW_POWER_ON: ret = states.ONEVIEW_POWER_ON else: ret = self.set_node_power_state(node_info, states.ONEVIEW_POWER_ON) return ret @auditing.audit def power_off(self, node_info): if self.get_node_power_state(node_info) == states.ONEVIEW_POWER_OFF: ret = states.ONEVIEW_POWER_OFF else: ret = self.set_node_power_state( node_info, states.ONEVIEW_POWER_OFF, PRESS_AND_HOLD ) return ret @auditing.audit def set_node_power_state( self, node_info, state, press_type=MOMENTARY_PRESS ): body = {'powerState': state, 'powerControl': press_type} power_state_uri = (node_info.get('server_hardware_uri') + '/powerState') task = self._prepare_and_do_request( uri=power_state_uri, body=body, request_type=PUT_REQUEST_TYPE ) try: self._wait_for_task_to_complete(task) except exceptions.OneViewTaskError as e: raise exceptions.OneViewErrorStateSettingPowerState(e.message) return state # --- Management Driver --- @auditing.audit def get_server_hardware(self, node_info): uuid = node_info['server_hardware_uri'].split("/")[-1] return self._server_hardware.get(uuid) @auditing.audit def get_server_hardware_by_uuid(self, uuid): return self._server_hardware.get(uuid) @auditing.audit def get_server_profile_from_hardware(self, node_info): server_hardware = self.get_server_hardware(node_info) server_profile_uri = server_hardware.server_profile_uri if server_profile_uri is None: message = ( "There is no server profile assigned to" " %(server_hardware_uri)s" % {'server_hardware_uri': node_info.get('server_hardware_uri')} ) raise exceptions.OneViewServerProfileAssociatedError(message) server_profile_uuid = server_profile_uri.split("/")[-1] return self._server_profile.get(server_profile_uuid) @auditing.audit def get_server_profile_template(self, node_info): uuid = node_info['server_profile_template_uri'].split("/")[-1] return self._server_profile_template.get(uuid) @auditing.audit def get_server_profile_template_by_uuid(self, uuid): return self._server_profile_template.get(uuid) @auditing.audit def get_server_profile_by_uuid(self, uuid): return self._server_profile.get(uuid) @auditing.audit def get_boot_order(self, node_info): server_profile = self.get_server_profile_from_hardware( node_info ) return server_profile.boot.get("order") @auditing.audit def set_boot_device(self, node_info, new_primary_boot_device, onetime=False): if new_primary_boot_device is None: raise exceptions.OneViewBootDeviceInvalidError() boot_order = self.get_boot_order(node_info) if new_primary_boot_device == boot_order[0]: return if onetime: try: sh_uuid = utils.get_uuid_from_uri( node_info['server_hardware_uri'] ) self._set_onetime_boot(sh_uuid, new_primary_boot_device) return except exceptions.IloException: # Falls back to persistent in case of failure pass self._persistent_set_boot_device(node_info, boot_order, new_primary_boot_device) @auditing.audit def _persistent_set_boot_device(self, node_info, boot_order, new_primary_boot_device): if new_primary_boot_device in boot_order: boot_order.remove(new_primary_boot_device) boot_order.insert(0, new_primary_boot_device) server_profile = self.get_server_profile_from_hardware( node_info ) manage_boot = server_profile.boot.get("manageBoot") server_profile.boot = { "manageBoot": manage_boot, "order": boot_order } boot_order_dict = server_profile.to_oneview_dict() task = self._prepare_and_do_request( uri=server_profile.uri, body=boot_order_dict, request_type=PUT_REQUEST_TYPE ) try: self._wait_for_task_to_complete(task) except exceptions.OneViewTaskError as e: raise exceptions.OneViewErrorSettingBootDevice(e.message) # ---- Deploy Driver ---- @auditing.audit def clone_template_and_apply(self, server_profile_name, server_hardware_uuid, server_profile_template_uuid): if not server_profile_name: raise ValueError( 'Missing Server Profile name.' ) if not server_hardware_uuid: raise ValueError( 'Missing Server Hardware uuid.' ) if not server_profile_template_uuid: raise ValueError( 'Missing Server Profile Template uuid.' ) server_hardware_uri = '%s%s' % ( SERVER_HARDWARE_PREFIX_URI, server_hardware_uuid ) server_profile_template_uri = '%s%s' % ( SERVER_PROFILE_TEMPLATE_PREFIX_URI, server_profile_template_uuid ) generate_new_profile_uri = '%s/new-profile' % ( server_profile_template_uri ) server_profile_from_template_json = self._prepare_and_do_request( uri=generate_new_profile_uri ) server_profile_from_template_json['serverHardwareUri'] = ( server_hardware_uri ) server_profile_from_template_json['name'] = server_profile_name server_profile_from_template_json['serverProfileTemplateUri'] = "" post_profile_task = self._prepare_and_do_request( uri=SERVER_PROFILE_PREFIX_URI, body=server_profile_from_template_json, request_type=POST_REQUEST_TYPE ) try: complete_task = self._wait_for_task_to_complete( post_profile_task ) except exceptions.OneViewTaskError as e: raise exceptions.OneViewServerProfileAssignmentError(e.message) server_profile_uri = ( complete_task.get('associatedResource').get('resourceUri') ) uuid = server_profile_uri.split("/")[-1] server_profile = self.get_server_profile_by_uuid(uuid) return server_profile @auditing.audit def delete_server_profile(self, uuid): if not uuid: raise ValueError('Missing Server Profile uuid.') delete_profile_task = self._prepare_and_do_request( uri='%s%s' % (SERVER_PROFILE_PREFIX_URI, uuid), request_type=DELETE_REQUEST_TYPE ) try: complete_task = self._wait_for_task_to_complete( delete_profile_task ) except exceptions.OneViewTaskError as e: raise exceptions.OneViewServerProfileDeletionError(e.message) return complete_task.get('associatedResource').get('resourceUri') # ---- Node Validate ---- @auditing.audit def validate_connections(self, oneview_info, ports, network_interface): if network_interface != 'neutron': self._is_node_port_mac_compatible_with_server_hardware( oneview_info, ports) self._validate_server_profile_template_mac_type(oneview_info) elif oneview_info.get('use_oneview_ml2_driver'): valid_ports = utils.get_oneview_connection_ports(ports) self._validate_node_and_port_server_hardware_uri( oneview_info, valid_ports) self._validate_connection_mac(oneview_info, valid_ports) def is_node_port_mac_compatible_with_server_hardware( self, node_info, ports ): # NOTE(mrtenio): While the connection validation patch # is not accepted, we need to refer to the new validation. self._is_node_port_mac_compatible_with_server_hardware( node_info, ports) @auditing.audit def _is_node_port_mac_compatible_with_server_hardware( self, node_info, ports ): if not ports: return server_hardware = self.get_server_hardware(node_info) try: mac = server_hardware.get_mac(nic_index=0) except exceptions.OneViewException: mac = self.get_sh_mac_from_ilo(server_hardware.uuid, nic_index=0) for port in ports: if port.address.lower() == mac: return message = ( "The ports of the node are not compatible with its " "server hardware %(server_hardware_uri)s." % {'server_hardware_uri': server_hardware.uri} ) raise exceptions.OneViewInconsistentResource(message) def validate_server_profile_template_mac_type(self, spt_uuid): info = {"server_profile_template_uri": spt_uuid} self._validate_server_profile_template_mac_type(info) @auditing.audit def _validate_server_profile_template_mac_type(self, oneview_info): server_profile_template = self.get_server_profile_template( oneview_info ) if server_profile_template.mac_type != 'Physical': message = ( "The server profile template %s is not set to use " "physical MAC." % server_profile_template.uri ) raise exceptions.OneViewInconsistentResource(message) @auditing.audit def _validate_node_and_port_server_hardware_uri(self, oneview_info, ports): node_hardware_id = utils.get_uuid_from_uri( oneview_info.get('server_hardware_uri') ) for port in ports: switch_info = utils.get_switch_info(port) port_hardware_id = switch_info.get('server_hardware_id') if port_hardware_id.lower() != node_hardware_id.lower(): raise exceptions.OneViewInconsistentResource( "The Server Hardware ID of the port %(port_id)s " "doesn't match the Server Hardware ID %(server_hardware)s " "of the node." % { 'port_id': port.uuid, 'server_hardware': node_hardware_id } ) @auditing.audit def _validate_connection_mac(self, oneview_info, ports): server_hardware = self.get_server_hardware(oneview_info) for port in ports: if port.address.lower() not in utils.get_all_macs(server_hardware): raise exceptions.OneViewInconsistentResource( "The MAC address %(mac)s of the port %(port_id)s doesn't" " have a corresponding MAC address in the Server Hardware" " %(server_hardware_uri)s" % { 'mac': port.address, 'port_id': port.uuid, 'server_hardware_uri': server_hardware.uri } ) @auditing.audit def validate_node_server_hardware( self, node_info, node_memorymb, node_cpus ): # NOTE(fellypefca): This is temporary to fix the problems of CI. pass @auditing.audit def validate_node_server_hardware_type(self, node_info): node_sht_uri = node_info.get('server_hardware_type_uri') server_hardware = self.get_server_hardware(node_info) server_hardware_sht_uri = server_hardware.server_hardware_type_uri if server_hardware_sht_uri != node_sht_uri: message = ( "Node server_hardware_type_uri is inconsistent" " with OneView's server hardware %(server_hardware_uri)s" " serverHardwareTypeUri." % {'server_hardware_uri': node_info.get('server_hardware_uri')} ) raise exceptions.OneViewInconsistentResource(message) @auditing.audit def check_server_profile_is_applied(self, node_info): self.get_server_profile_from_hardware(node_info) @auditing.audit def validate_node_enclosure_group(self, node_info): server_hardware = self.get_server_hardware(node_info) sh_enclosure_group_uri = server_hardware.enclosure_group_uri node_enclosure_group_uri = node_info.get('enclosure_group_uri') if node_enclosure_group_uri not in ('', 'None', None): if sh_enclosure_group_uri != node_enclosure_group_uri: message = ( "Node enclosure_group_uri '%(node_enclosure_group_uri)s' " "is inconsistent with OneView's server hardware " "serverGroupUri '%(sh_enclosure_group_uri)s' of " "ServerHardware %(server_hardware)s" % { 'node_enclosure_group_uri': node_enclosure_group_uri, 'sh_enclosure_group_uri': sh_enclosure_group_uri, 'server_hardware': server_hardware.uuid, } ) raise exceptions.OneViewInconsistentResource(message) @auditing.audit def is_node_port_mac_compatible_with_server_profile( self, node_info, ports ): server_profile = self.get_server_profile_from_hardware( node_info ) if server_profile.connections: primary_boot_connection = None for connection in server_profile.connections: boot = connection.get('boot') if (boot is not None and boot.get('priority').lower() == 'primary'): primary_boot_connection = connection if primary_boot_connection is None: message = ( "No primary boot connection configured for server profile" " %s." % server_profile.uri ) raise exceptions.OneViewInconsistentResource(message) mac = primary_boot_connection.get('mac') else: # If no connections on Server Profile, the connections are not # being managed or the server is rack based. In both cases, fall # back to 1st nic from iLO server_hardware = self.get_server_hardware(node_info) mac = self.get_sh_mac_from_ilo(server_hardware.uuid, nic_index=0) for port in ports: if port.address.lower() == mac.lower(): return message = ( "The ports of the node are not compatible with its" " server profile %(server_profile_uri)s." % {'server_profile_uri': server_profile.uri} ) raise exceptions.OneViewInconsistentResource(message) @auditing.audit def validate_node_server_profile_template(self, node_info): server_profile_template = self.get_server_profile_template(node_info) server_hardware = self.get_server_hardware(node_info) self._validate_spt_server_hardware_type( server_profile_template, server_hardware ) self._validate_spt_enclosure_group( server_profile_template, server_hardware ) self._validate_spt_manage_boot(server_profile_template) @auditing.audit def _validate_spt_server_hardware_type( self, server_profile_template, server_hardware ): spt_server_hardware_type_uri = ( server_profile_template.server_hardware_type_uri ) sh_server_hardware_type_uri = server_hardware.server_hardware_type_uri if spt_server_hardware_type_uri != sh_server_hardware_type_uri: message = ( "Server profile template %(spt_uri)s serverHardwareTypeUri is" " inconsistent with server hardware %(server_hardware_uri)s" " serverHardwareTypeUri." % {'spt_uri': server_profile_template.uri, 'server_hardware_uri': server_hardware.uri} ) raise exceptions.OneViewInconsistentResource(message) @auditing.audit def _validate_spt_enclosure_group( self, server_profile_template, server_hardware ): spt_enclosure_group_uri = server_profile_template.enclosure_group_uri sh_enclosure_group_uri = server_hardware.enclosure_group_uri if spt_enclosure_group_uri != sh_enclosure_group_uri: message = ( "Server profile template %(spt_uri)s enclosureGroupUri is" " inconsistent with server hardware %(server_hardware_uri)s" " serverGroupUri." % {'spt_uri': server_profile_template.uri, 'server_hardware_uri': server_hardware.uri} ) raise exceptions.OneViewInconsistentResource(message) @auditing.audit def _validate_spt_manage_boot(self, server_profile_template): manage_boot = server_profile_template.boot.get('manageBoot') if not manage_boot: message = ( "Server Profile Template: %s, does not allow to manage " "boot order." % server_profile_template.uri ) raise exceptions.OneViewInconsistentResource(message) @auditing.audit def _validate_spt_boot_connections(self, server_profile_template): spt_connections = server_profile_template.connections for connection in spt_connections: boot = connection.get('boot') if boot and boot.get('priority').lower() == 'primary': return message = ( "No primary boot connection configured for server profile" " template %s." % server_profile_template.uri ) raise exceptions.OneViewInconsistentResource(message)