def get_parser(self, prog_name): parser = super(RebootBaremetalNode, self).get_parser(prog_name) parser.add_argument( 'node', metavar='<node>', help="Name or UUID of the node." ) parser.add_argument( '--soft', dest='soft', action='store_true', default=False, help=_("Request Graceful reboot.") ) parser.add_argument( '--power-timeout', metavar='<power-timeout>', default=None, type=int, help=_("Timeout (in seconds, positive integer) to wait for the " "target power state before erroring out.") ) return parser
def get_parser(self, prog_name): parser = super(UnsetBaremetalPort, self).get_parser(prog_name) parser.add_argument( 'port', metavar='<port>', help=_("UUID of the port.") ) parser.add_argument( "--extra", metavar="<key>", action='append', help=_('Extra to unset on this baremetal port ' '(repeat option to unset multiple extras)') ) parser.add_argument( '--port-group', action='store_true', dest='portgroup', help=_("Remove port from the port group")) parser.add_argument( '--physical-network', action='store_true', dest='physical_network', help=_("Unset the physical network on this baremetal port.")) parser.add_argument( '--is-smartnic', dest='is_smartnic', action='store_true', help=_("Set Port as not Smart NIC port")) return parser
def set_power_state(self, node_id, state, soft=False, timeout=None): """Sets power state for a node. :param node_id: Node identifier :param state: One of target power state, 'on', 'off', or 'reboot' :param soft: The flag for graceful power 'off' or 'reboot' :param timeout: The timeout (in seconds) positive integer value (> 0) :raises: ValueError if 'soft' or 'timeout' option is invalid :returns: The status of the request """ if state == 'on' and soft: raise ValueError( _("'soft' option is invalid for the power-state 'on'")) path = "%s/states/power" % node_id requested_state = 'soft ' + state if soft else state target = _power_states.get(requested_state, state) body = {'target': target} if timeout is not None: msg = _("'timeout' option for setting power state must have " "positive integer value (> 0)") try: timeout = int(timeout) except (ValueError, TypeError): raise ValueError(msg) if timeout <= 0: raise ValueError(msg) body = {'target': target, 'timeout': timeout} return self.update(path, body, http_method='PUT')
def handle_json_or_file_arg(json_arg): """Attempts to read JSON argument from file or string. :param json_arg: May be a file name containing the JSON, or a JSON string. :returns: A list or dictionary parsed from JSON. :raises: InvalidAttribute if the argument cannot be parsed. """ if os.path.isfile(json_arg): try: with open(json_arg, 'r') as f: json_arg = f.read().strip() except Exception as e: err = _("Cannot get JSON from file '%(file)s'. " "Error: %(err)s") % {'err': e, 'file': json_arg} raise exc.InvalidAttribute(err) try: json_arg = json.loads(json_arg) except ValueError as e: err = (_("For JSON: '%(string)s', error: '%(err)s'") % {'err': e, 'string': json_arg}) raise exc.InvalidAttribute(err) return json_arg
def wait(self, allocation_id, timeout=0, poll_interval=1, poll_delay_function=None): """Wait for the Allocation to become active. :param timeout: timeout in seconds, no timeout if 0. :param poll_interval: interval in seconds between polls. :param poll_delay_function: function to use to wait between polls (defaults to time.sleep). Should take one argument - delay time in seconds. Any exceptions raised inside it will abort the wait. :return: updated :class:`Allocation` object. :raises: StateTransitionFailed if allocation reaches the error state. :raises: StateTransitionTimeout on timeout. """ timeout_msg = _('Allocation %(allocation)s failed to become active ' 'in %(timeout)s seconds') % { 'allocation': allocation_id, 'timeout': timeout} for _count in utils.poll(timeout, poll_interval, poll_delay_function, timeout_msg): allocation = self.get(allocation_id) if allocation.state == 'error': raise exc.StateTransitionFailed( _('Allocation %(allocation)s failed: %(error)s') % {'allocation': allocation_id, 'error': allocation.last_error}) elif allocation.state == 'active': return allocation LOG.debug('Still waiting for allocation %(allocation)s to become ' 'active, the current state is %(actual)s', {'allocation': allocation_id, 'actual': allocation.state})
def do_node_set_provision_state(cc, args): """Initiate a provisioning state change for a node.""" if args.config_drive and args.provision_state != 'active': raise exceptions.CommandError(_('--config-drive is only valid when ' 'setting provision state to "active"')) elif args.clean_steps and args.provision_state != 'clean': raise exceptions.CommandError(_('--clean-steps is only valid when ' 'setting provision state to "clean"')) elif args.provision_state == 'clean' and not args.clean_steps: raise exceptions.CommandError(_('--clean-steps must be specified when ' 'setting provision state to "clean"')) if args.wait_timeout is not None: wait_args = v1_utils.PROVISION_ACTIONS.get(args.provision_state) if wait_args is None: raise exceptions.CommandError( _("--wait is not supported for provision state '%s'") % args.provision_state) clean_steps = args.clean_steps if args.clean_steps == '-': clean_steps = utils.get_from_stdin('clean steps') if clean_steps: clean_steps = utils.handle_json_or_file_arg(clean_steps) cc.node.set_provision_state(args.node, args.provision_state, configdrive=args.config_drive, cleansteps=clean_steps) if args.wait_timeout is not None: print(_('Waiting for provision state %(state)s on node %(node)s') % {'state': wait_args['expected_state'], 'node': args.node}) cc.node.wait_for_provision_state(args.node, timeout=args.wait_timeout, **wait_args)
def get_parser(self, prog_name): parser = super(PassthruCallBaremetalDriver, self).get_parser(prog_name) parser.add_argument( 'driver', metavar='<driver>', help=_('Name of the driver.') ) parser.add_argument( 'method', metavar='<method>', help=_("Vendor passthru method to be called.") ) parser.add_argument( '--arg', metavar='<key=value>', action='append', help=_("Argument to pass to the passthru method (repeat option " "to specify multiple arguments).") ) parser.add_argument( '--http-method', dest='http_method', metavar='<http-method>', choices=v1_utils.HTTP_METHODS, default='POST', help=_("The HTTP method to use in the passthru request. One of " "%s. Defaults to 'POST'.") % oscutils.format_list(v1_utils.HTTP_METHODS) ) return parser
def get_parser(self, prog_name): parser = ( super(SetBaremetalVolumeConnector, self).get_parser(prog_name)) parser.add_argument( 'volume_connector', metavar='<volume connector>', help=_("UUID of the volume connector.")) parser.add_argument( '--node', dest='node_uuid', metavar='<uuid>', help=_('UUID of the node that this volume connector belongs to.')) parser.add_argument( '--type', dest='type', metavar="<type>", choices=('iqn', 'ip', 'mac', 'wwnn', 'wwpn', 'port', 'portgroup'), help=_("Type of the volume connector. Can be 'iqn', 'ip', 'mac', " "'wwnn', 'wwpn', 'port', 'portgroup'.")) parser.add_argument( '--connector-id', dest='connector_id', metavar="<connector id>", help=_("ID of the volume connector in the specified type.")) parser.add_argument( '--extra', dest='extra', metavar="<key=value>", action='append', help=_("Record arbitrary key/value metadata. " "Can be specified multiple times.")) return parser
def get_parser(self, prog_name): parser = super(UnsetBaremetalPortGroup, self).get_parser(prog_name) parser.add_argument( 'portgroup', metavar='<port group>', help=_("Name or UUID of the port group.") ) parser.add_argument( "--name", action='store_true', help=_("Unset the name of the port group."), ) parser.add_argument( "--address", action='store_true', help=_("Unset the address of the port group."), ) parser.add_argument( "--extra", metavar="<key>", action='append', help=_('Extra to unset on this baremetal port group ' '(repeat option to unset multiple extras).'), ) parser.add_argument( "--property", dest='properties', metavar="<key>", action='append', help=_('Property to unset on this baremetal port group ' '(repeat option to unset multiple properties).'), ) return parser
def _handle_clean_steps_arg(clean_steps): """Attempts to read clean steps argument. :param clean_steps: May be a file name containing the clean steps, or a JSON string representing the clean steps. :returns: A list of dictionaries representing clean steps. :raises: InvalidAttribute if the argument cannot be parsed. """ if os.path.isfile(clean_steps): try: with open(clean_steps, 'r') as f: clean_steps = f.read().strip() except Exception as e: err = _("Cannot get clean steps from file '%(file)s'. " "Error: %(err)s") % {'err': e, 'file': clean_steps} raise exc.InvalidAttribute(err) try: clean_steps = json.loads(clean_steps) except ValueError as e: err = (_("For clean steps: '%(steps)s', error: '%(err)s'") % {'err': e, 'steps': clean_steps}) raise exc.InvalidAttribute(err) return clean_steps
def get_parser(self, prog_name): parser = super(ShowBaremetalPort, self).get_parser(prog_name) parser.add_argument( "port", metavar="<id>", help=_("UUID of the port (or MAC address if --address is " "specified).") ) parser.add_argument( '--address', dest='address', action='store_true', default=False, help=_('<id> is the MAC address (instead of the UUID) of the ' 'port.') ) parser.add_argument( '--fields', nargs='+', dest='fields', metavar='<field>', action='append', choices=res_fields.PORT_DETAILED_RESOURCE.fields, default=[], help=_("One or more port fields. Only these fields will be " "fetched from the server.") ) return parser
def get_parser(self, prog_name): parser = super(PowerBaremetalNode, self).get_parser(prog_name) parser.add_argument( 'power_state', metavar='<on|off>', choices=['on', 'off'], help="Power node on or off" ) parser.add_argument( 'node', metavar='<node>', help="Name or UUID of the node." ) parser.add_argument( '--soft', dest='soft', action='store_true', default=False, help=_("Request graceful power-off.") ) parser.add_argument( '--power-timeout', metavar='<power-timeout>', default=None, type=int, help=_("Timeout (in seconds, positive integer) to wait for the " "target power state before erroring out.") ) return parser
def do_node_delete(cc, args): """Unregister node(s) from the Ironic service.""" for n in args.node: try: cc.node.delete(n) print(_('Deleted node %s') % n) except exceptions.ClientException as e: print(_("Failed to delete node %(node)s: %(error)s") % {'node': n, 'error': e})
def get_parser(self, prog_name): parser = super(SetBaremetalPortGroup, self).get_parser(prog_name) parser.add_argument( 'portgroup', metavar='<port group>', help=_("Name or UUID of the port group."), ) parser.add_argument( '--node', dest='node_uuid', metavar='<uuid>', help=_('Update UUID of the node that this port group belongs to.') ) parser.add_argument( "--address", metavar="<mac-address>", help=_("MAC address for this port group."), ) parser.add_argument( "--name", metavar="<name>", help=_("Name of the port group."), ) parser.add_argument( "--extra", metavar="<key=value>", action='append', help=_('Extra to set on this baremetal port group ' '(repeat option to set multiple extras).'), ) parser.add_argument( '--mode', help=_('Mode of the port group. For possible values, refer to ' 'https://www.kernel.org/doc/Documentation/networking' '/bonding.txt.')) parser.add_argument( '--property', dest='properties', metavar="<key=value>", action='append', help=_("Key/value property related to this port group's " "configuration (repeat option to set multiple " "properties).")) standalone_ports_group = parser.add_mutually_exclusive_group() standalone_ports_group.add_argument( '--support-standalone-ports', action='store_true', default=None, help=_("Ports that are members of this port group " "can be used as stand-alone ports.") ) standalone_ports_group.add_argument( '--unsupport-standalone-ports', action='store_true', help=_("Ports that are members of this port group " "cannot be used as stand-alone ports.") ) return parser
def do_port_delete(cc, args): """Delete a port.""" failures = [] for p in args.port: try: cc.port.delete(p) print(_('Deleted port %s') % p) except exceptions.ClientException as e: failures.append(_("Failed to delete port %(port)s: %(error)s") % {'port': p, 'error': e}) if failures: raise exceptions.ClientException("\n".join(failures))
def do_volume_target_delete(cc, args): """Delete a volume target.""" failures = [] for vt in args.volume_target: try: cc.volume_target.delete(vt) print(_('Deleted volume target %s') % vt) except exceptions.ClientException as e: failures.append(_("Failed to delete volume target %(vt)s: " "%(error)s") % {'vt': vt, 'error': e}) if failures: raise exceptions.ClientException("\n".join(failures))
def do_volume_connector_delete(cc, args): """Delete a volume connector.""" failures = [] for vc in args.volume_connector: try: cc.volume_connector.delete(vc) print(_('Deleted volume connector %s') % vc) except exceptions.ClientException as e: failures.append(_("Failed to delete volume connector %(vc)s: " "%(error)s") % {'vc': vc, 'error': e}) if failures: raise exceptions.ClientException("\n".join(failures))
def take_action(self, parsed_args): self.log.debug("take_action(%s)" % parsed_args) client = self.app.client_manager.baremetal columns = res_fields.NODE_RESOURCE params = {} if parsed_args.limit is not None and parsed_args.limit < 0: raise exc.CommandError( _('Expected non-negative --limit, got %s') % parsed_args.limit) params['limit'] = parsed_args.limit params['marker'] = parsed_args.marker if parsed_args.associated: params['associated'] = parsed_args.associated if parsed_args.maintenance: params['maintenance'] = parsed_args.maintenance if parsed_args.long: columns = res_fields.NODE_DETAILED_RESOURCE params['detail'] = parsed_args.long self.log.debug("params(%s)" % params) data = client.node.list(**params) data = oscutils.sort_items(data, parsed_args.sort) return (columns.labels, (oscutils.get_item_properties(s, columns.fields, formatters={ 'Properties': oscutils.format_dict},) for s in data))
def do_node_set_maintenance(cc, args): """Enable or disable maintenance mode for a node.""" if args.reason and args.maintenance_mode.lower() in ('false', 'off'): raise exceptions.CommandError(_('Cannot set "reason" when turning off ' 'maintenance mode.')) cc.node.set_maintenance(args.node, args.maintenance_mode.lower(), maint_reason=args.reason)
def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) client = self.app.client_manager.baremetal columns = res_fields.CHASSIS_RESOURCE.fields labels = res_fields.CHASSIS_RESOURCE.labels params = {} if parsed_args.limit is not None and parsed_args.limit < 0: raise exc.CommandError( _('Expected non-negative --limit, got %s') % parsed_args.limit) params['limit'] = parsed_args.limit params['marker'] = parsed_args.marker if parsed_args.long: params['detail'] = parsed_args.long columns = res_fields.CHASSIS_DETAILED_RESOURCE.fields labels = res_fields.CHASSIS_DETAILED_RESOURCE.labels elif parsed_args.fields: params['detail'] = False fields = itertools.chain.from_iterable(parsed_args.fields) resource = res_fields.Resource(list(fields)) columns = resource.fields labels = resource.labels params['fields'] = columns self.log.debug("params(%s)", params) data = client.chassis.list(**params) data = oscutils.sort_items(data, parsed_args.sort) return (labels, (oscutils.get_item_properties(s, columns, formatters={ 'Properties': oscutils.format_dict},) for s in data))
def _discover_auth_versions(self, session, auth_url): # discover the API versions the server is supporting base on the # given URL v2_auth_url = None v3_auth_url = None try: ks_discover = discover.Discover(session=session, auth_url=auth_url) v2_auth_url = ks_discover.url_for('2.0') v3_auth_url = ks_discover.url_for('3.0') except ks_exc.ClientException: # Identity service may not support discover API version. # Let's try to figure out the API version from the original URL. url_parts = urlparse.urlparse(auth_url) (scheme, netloc, path, params, query, fragment) = url_parts path = path.lower() if path.startswith('/v3'): v3_auth_url = auth_url elif path.startswith('/v2'): v2_auth_url = auth_url else: # not enough information to determine the auth version msg = _('Unable to determine the Keystone version ' 'to authenticate with using the given ' 'auth_url. Identity service may not support API ' 'version discovery. Please provide a versioned ' 'auth_url instead. %s') % auth_url raise exc.CommandError(msg) return (v2_auth_url, v3_auth_url)
def get_client(api_version, **kwargs): """Get an authenticated client, based on the credentials in args. :param api_version: the API version to use. Valid value: '1'. :param kwargs: keyword args containing credentials, either: * os_auth_token: pre-existing token to re-use * ironic_url: ironic API endpoint or: * os_username: name of user * os_password: user's password * os_auth_url: endpoint to authenticate against * insecure: allow insecure SSL (no cert verification) * os_tenant_{name|id}: name or ID of tenant """ if kwargs.get("os_auth_token") and kwargs.get("ironic_url"): token = kwargs.get("os_auth_token") endpoint = kwargs.get("ironic_url") auth_ref = None elif ( kwargs.get("os_username") and kwargs.get("os_password") and kwargs.get("os_auth_url") and (kwargs.get("os_tenant_id") or kwargs.get("os_tenant_name")) ): ks_kwargs = { "username": kwargs.get("os_username"), "password": kwargs.get("os_password"), "tenant_id": kwargs.get("os_tenant_id"), "tenant_name": kwargs.get("os_tenant_name"), "auth_url": kwargs.get("os_auth_url"), "service_type": kwargs.get("os_service_type"), "endpoint_type": kwargs.get("os_endpoint_type"), "insecure": kwargs.get("insecure"), } _ksclient = _get_ksclient(**ks_kwargs) token = kwargs.get("os_auth_token") if kwargs.get("os_auth_token") else _ksclient.auth_token ks_kwargs["region_name"] = kwargs.get("os_region_name") endpoint = kwargs.get("ironic_url") or _get_endpoint(_ksclient, **ks_kwargs) auth_ref = _ksclient.auth_ref else: e = _("Must provide Keystone credentials or user-defined endpoint " "and token") raise exc.AmbiguousAuthSystem(e) cli_kwargs = { "token": token, "insecure": kwargs.get("insecure"), "timeout": kwargs.get("timeout"), "ca_file": kwargs.get("ca_file"), "cert_file": kwargs.get("cert_file"), "key_file": kwargs.get("key_file"), "auth_ref": auth_ref, "os_ironic_api_version": kwargs.get("os_ironic_api_version"), } return Client(api_version, endpoint, **cli_kwargs)
def get_parser(self, prog_name): parser = ( super(UnsetBaremetalVolumeConnector, self).get_parser(prog_name)) parser.add_argument( 'volume_connector', metavar='<volume connector>', help=_("UUID of the volume connector.")) parser.add_argument( '--extra', dest='extra', metavar="<key>", action='append', help=_('Extra to unset (repeat option to unset multiple extras)')) return parser
def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) baremetal_client = self.app.client_manager.baremetal failures = [] for port in parsed_args.ports: try: baremetal_client.port.delete(port) print(_('Deleted port %s') % port) except exc.ClientException as e: failures.append(_("Failed to delete port %(port)s: %(error)s") % {'port': port, 'error': e}) if failures: raise exc.ClientException("\n".join(failures))
def do_node_set_provision_state(cc, args): """Provision, rebuild, delete, inspect, provide or manage an instance.""" if args.config_drive and args.provision_state != 'active': raise exceptions.CommandError(_('--config-drive is only valid when ' 'setting provision state to "active"')) cc.node.set_provision_state(args.node, args.provision_state, configdrive=args.config_drive)
def vendor_passthru(self, node_id, method, args=None, http_method=None): """Issue requests for vendor-specific actions on a given node. :param node_id: The UUID of the node. :param method: Name of the vendor method. :param args: Optional. The arguments to be passed to the method. :param http_method: The HTTP method to use on the request. Defaults to POST. """ if args is None: args = {} if http_method is None: http_method = 'POST' http_method = http_method.upper() path = "%s/vendor_passthru/%s" % (node_id, method) if http_method in ('POST', 'PUT', 'PATCH'): return self.update(path, args, http_method=http_method) elif http_method == 'DELETE': return self.delete(path) elif http_method == 'GET': return self.get(path) else: raise exc.InvalidAttribute( _('Unknown HTTP method: %s') % http_method)
def set_maintenance(self, node_id, state, maint_reason=None): """Set the maintenance mode for the node. :param node_id: The UUID of the node. :param state: the maintenance mode; either a Boolean or a string representation of a Boolean (eg, 'true', 'on', 'false', 'off'). True to put the node in maintenance mode; False to take the node out of maintenance mode. :param maint_reason: Optional string. Reason for putting node into maintenance mode. :raises: InvalidAttribute if state is an invalid string (that doesn't represent a Boolean). """ if isinstance(state, bool): maintenance_mode = state else: try: maintenance_mode = strutils.bool_from_string(state, True) except ValueError as e: raise exc.InvalidAttribute(_("Argument 'state': %(err)s") % {'err': e}) path = "%s/maintenance" % node_id if maintenance_mode: reason = {'reason': maint_reason} return self.update(path, reason, http_method='PUT') else: return self.delete(path)
def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) baremetal_client = self.app.client_manager.baremetal failures = [] for node in parsed_args.nodes: try: baremetal_client.node.delete(node) print(_('Deleted node %s') % node) except exc.ClientException as e: failures.append(_("Failed to delete node %(node)s: %(error)s") % {'node': node, 'error': e}) if failures: raise exc.ClientException("\n".join(failures))
def _check_version(self, api_version): if api_version == 'latest': return LATEST_API_VERSION else: try: versions = tuple(int(i) for i in api_version.split('.')) except ValueError: versions = () if len(versions) == 1: # Default value of ironic_api_version is '1'. # If user not specify the value of api version, not passing # headers at all. os_ironic_api_version = None elif len(versions) == 2: os_ironic_api_version = api_version # In the case of '1.0' if versions[1] == 0: os_ironic_api_version = None else: msg = _("The requested API version %(ver)s is an unexpected " "format. Acceptable formats are 'X', 'X.Y', or the " "literal string '%(latest)s'." ) % {'ver': api_version, 'latest': 'latest'} raise exc.CommandError(msg) api_major_version = versions[0] return (api_major_version, os_ironic_api_version)
def __init__(self, endpoint, **kwargs): self.endpoint = endpoint self.endpoint_trimmed = _trim_endpoint_api_version(endpoint) self.auth_token = kwargs.get('token') self.auth_ref = kwargs.get('auth_ref') self.os_ironic_api_version = kwargs.get('os_ironic_api_version', DEFAULT_VER) self.api_version_select_state = kwargs.get( 'api_version_select_state', 'default') self.conflict_max_retries = kwargs.pop('max_retries', DEFAULT_MAX_RETRIES) self.conflict_retry_interval = kwargs.pop('retry_interval', DEFAULT_RETRY_INTERVAL) self.session = requests.Session() parts = urlparse.urlparse(endpoint) if parts.scheme not in SUPPORTED_ENDPOINT_SCHEME: msg = _('Unsupported scheme: %s') % parts.scheme raise exc.EndpointException(msg) if parts.scheme == 'https': if kwargs.get('insecure') is True: self.session.verify = False elif kwargs.get('ca_file'): self.session.verify = kwargs['ca_file'] self.session.cert = (kwargs.get('cert_file'), kwargs.get('key_file'))
def get_parser(self, prog_name): parser = super(CreateBaremetalPort, self).get_parser(prog_name) parser.add_argument('address', metavar='<address>', help=_('MAC address for this port.')) parser.add_argument( '--node', dest='node_uuid', metavar='<uuid>', required=True, help=_('UUID of the node that this port belongs to.')) parser.add_argument('--extra', metavar="<key=value>", action='append', help=_("Record arbitrary key/value metadata. " "Can be specified multiple times.")) parser.add_argument( '--local-link-connection', metavar="<key=value>", action='append', help=_("Key/value metadata describing Local link connection " "information. Valid keys are switch_info, switch_id, " "port_id; switch_id and port_id are obligatory. Can be " "specified multiple times.")) parser.add_argument( '-l', dest='local_link_connection_deprecated', metavar="<key=value>", action='append', help=_("DEPRECATED. Please use --local-link-connection instead. " "Key/value metadata describing Local link connection " "information. Valid keys are switch_info, switch_id, " "port_id; switch_id and port_id are obligatory. Can be " "specified multiple times.")) parser.add_argument( '--pxe-enabled', metavar='<boolean>', help=_('Indicates whether this Port should be used when ' 'PXE booting this Node.')) parser.add_argument( '--port-group', dest='portgroup_uuid', metavar='<uuid>', help=_("UUID of the port group that this port belongs to.")) return parser
def vendor_passthru(self, driver_name, method, args=None, http_method=None, os_ironic_api_version=None, global_request_id=None): """Issue requests for vendor-specific actions on a given driver. :param driver_name: The name of the driver. :param method: Name of the vendor method. :param args: Optional. The arguments to be passed to the method. :param http_method: The HTTP method to use on the request. Defaults to POST. :param os_ironic_api_version: String version (e.g. "1.35") to use for the request. If not specified, the client's default is used. :param global_request_id: String containing global request ID header value (in form "req-<UUID>") to use for the request. """ if args is None: args = {} if http_method is None: http_method = 'POST' http_method = http_method.upper() header_values = { "os_ironic_api_version": os_ironic_api_version, "global_request_id": global_request_id } path = "%s/vendor_passthru/%s" % (driver_name, method) if http_method in ('POST', 'PUT', 'PATCH'): return self.update(path, args, http_method=http_method, **header_values) elif http_method == 'DELETE': return self.delete(path, **header_values) elif http_method == 'GET': return self.get(path, **header_values) else: raise exc.InvalidAttribute( _('Unknown HTTP method: %s') % http_method)
def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) client = self.app.client_manager.baremetal columns = res_fields.NODE_RESOURCE.fields labels = res_fields.NODE_RESOURCE.labels params = {} if parsed_args.limit is not None and parsed_args.limit < 0: raise exc.CommandError( _('Expected non-negative --limit, got %s') % parsed_args.limit) params['limit'] = parsed_args.limit params['marker'] = parsed_args.marker if parsed_args.associated: params['associated'] = parsed_args.associated if parsed_args.maintenance: params['maintenance'] = parsed_args.maintenance if parsed_args.provision_state: params['provision_state'] = parsed_args.provision_state if parsed_args.resource_class: params['resource_class'] = parsed_args.resource_class if parsed_args.chassis: params['chassis'] = parsed_args.chassis if parsed_args.long: params['detail'] = parsed_args.long columns = res_fields.NODE_DETAILED_RESOURCE.fields labels = res_fields.NODE_DETAILED_RESOURCE.labels elif parsed_args.fields: params['detail'] = False fields = itertools.chain.from_iterable(parsed_args.fields) resource = res_fields.Resource(list(fields)) columns = resource.fields labels = resource.labels params['fields'] = columns self.log.debug("params(%s)", params) data = client.node.list(**params) data = oscutils.sort_items(data, parsed_args.sort) return (labels, (oscutils.get_item_properties( s, columns, formatters={'Properties': oscutils.format_dict}, ) for s in data))
def find(self, **kwargs): """Find a single item with attributes matching ``**kwargs``. This isn't very efficient: it loads the entire list then filters on the Python side. """ matches = self.findall(**kwargs) num_matches = len(matches) if num_matches == 0: msg = _("No %(name)s matching %(args)s.") % { 'name': self.resource_class.__name__, 'args': kwargs } raise exceptions.NotFound(msg) elif num_matches > 1: raise exceptions.NoUniqueMatch() else: return matches[0]
def get_client_class(version): version_map = { '1': 'ironicclient.v1.client.Client', '1.5': 'ironicclient.v1.client.Client', '1.6': 'ironicclient.v1.client.Client', '1.9': 'ironicclient.v1.client.Client', } try: client_path = version_map[str(version)] except (KeyError, ValueError): msg = _("Invalid client version '%(version)s'. must be one of: " "%(keys)s") % { 'version': version, 'keys': ', '.join(version_map) } raise exceptions.UnsupportedVersion(msg) return importutils.import_class(client_path)
def check_for_invalid_fields(fields, valid_fields): """Check for invalid fields. :param fields: A list of fields specified by the user. :param valid_fields: A list of valid fields. :raises CommandError: If invalid fields were specified by the user. """ if not fields: return invalid_fields = set(fields) - set(valid_fields) if invalid_fields: raise exc.CommandError( _('Invalid field(s) requested: %(invalid)s. Valid fields ' 'are: %(valid)s.') % { 'invalid': ', '.join(invalid_fields), 'valid': ', '.join(valid_fields) })
def get_parser(self, prog_name): parser = super(CreateBaremetalVolumeTarget, self).get_parser(prog_name) parser.add_argument( '--node', dest='node_uuid', metavar='<uuid>', required=True, help=_('UUID of the node that this volume target belongs to.')) parser.add_argument( '--type', dest='volume_type', metavar="<volume type>", required=True, help=_("Type of the volume target, e.g. 'iscsi', 'fibre_channel', " "'rbd'.")) parser.add_argument( '--property', dest='properties', metavar="<key=value>", action='append', help=_("Key/value property related to the type of this volume " "target. Can be specified multiple times.")) parser.add_argument('--boot-index', dest='boot_index', metavar="<boot index>", type=int, required=True, help=_("Boot index of the volume target.")) parser.add_argument( '--volume-id', dest='volume_id', metavar="<volume id>", required=True, help=_("ID of the volume associated with this target.")) parser.add_argument('--uuid', dest='uuid', metavar='<uuid>', help=_("UUID of the volume target.")) parser.add_argument('--extra', dest='extra', metavar="<key=value>", action='append', help=_("Record arbitrary key/value metadata. " "Can be specified multiple times.")) return parser
def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) client = self.app.client_manager.baremetal columns = res_fields.PORT_RESOURCE.fields labels = res_fields.PORT_RESOURCE.labels params = {} if parsed_args.limit is not None and parsed_args.limit < 0: raise exc.CommandError( _('Expected non-negative --limit, got %s') % parsed_args.limit) params['limit'] = parsed_args.limit params['marker'] = parsed_args.marker if parsed_args.address is not None: params['address'] = parsed_args.address if parsed_args.node is not None: params['node'] = parsed_args.node if parsed_args.portgroup is not None: params['portgroup'] = parsed_args.portgroup if parsed_args.detail: params['detail'] = parsed_args.detail columns = res_fields.PORT_DETAILED_RESOURCE.fields labels = res_fields.PORT_DETAILED_RESOURCE.labels elif parsed_args.fields: params['detail'] = False fields = itertools.chain.from_iterable(parsed_args.fields) resource = res_fields.Resource(list(fields)) columns = resource.fields labels = resource.labels params['fields'] = columns self.log.debug("params(%s)", params) data = client.port.list(**params) data = oscutils.sort_items(data, parsed_args.sort) return (labels, (oscutils.get_item_properties( s, columns, formatters={'extra': oscutils.format_dict}, ) for s in data))
def set_maintenance(self, node_id, state, maint_reason=None, os_ironic_api_version=None, global_request_id=None): """Set the maintenance mode for the node. :param node_id: The UUID of the node. :param state: the maintenance mode; either a Boolean or a string representation of a Boolean (eg, 'true', 'on', 'false', 'off'). True to put the node in maintenance mode; False to take the node out of maintenance mode. :param maint_reason: Optional string. Reason for putting node into maintenance mode. :param os_ironic_api_version: String version (e.g. "1.35") to use for the request. If not specified, the client's default is used. :param global_request_id: String containing global request ID header value (in form "req-<UUID>") to use for the request. :raises: InvalidAttribute if state is an invalid string (that doesn't represent a Boolean). """ if isinstance(state, bool): maintenance_mode = state else: try: maintenance_mode = strutils.bool_from_string(state, True) except ValueError as e: raise exc.InvalidAttribute( _("Argument 'state': %(err)s") % {'err': e}) path = "%s/maintenance" % node_id header_values = { "os_ironic_api_version": os_ironic_api_version, "global_request_id": global_request_id } if maintenance_mode: reason = {'reason': maint_reason} return self.update(path, reason, http_method='PUT', **header_values) else: return self.delete(path, **header_values)
def get_parser(self, prog_name): parser = (super(ListBaremetalVolumeConnector, self).get_parser(prog_name)) parser.add_argument( '--node', dest='node', metavar='<node>', help=_("Only list volume connectors of this node (name or UUID).")) parser.add_argument( '--limit', dest='limit', metavar='<limit>', type=int, help=_('Maximum number of volume connectors to return per ' 'request, 0 for no limit. Default is the maximum number ' 'used by the Baremetal API Service.')) parser.add_argument( '--marker', dest='marker', metavar='<volume connector>', help=_('Volume connector UUID (for example, of the last volume ' 'connector in the list from a previous request). Returns ' 'the list of volume connectors after this UUID.')) parser.add_argument( '--sort', dest='sort', metavar='<key>[:<direction>]', help=_('Sort output by specified volume connector fields and ' 'directions (asc or desc) (default:asc). Multiple fields ' 'and directions can be specified, separated by comma.')) display_group = parser.add_mutually_exclusive_group(required=False) display_group.add_argument( '--long', dest='detail', action='store_true', default=False, help=_("Show detailed information about volume connectors.")) display_group.add_argument( '--fields', nargs='+', dest='fields', metavar='<field>', action='append', default=[], choices=res_fields.VOLUME_CONNECTOR_DETAILED_RESOURCE.fields, help=_("One or more volume connector fields. Only these fields " "will be fetched from the server. Can not be used when " "'--long' is specified.")) return parser
class HttpError(ClientException): """The base exception class for all HTTP exceptions.""" http_status = 0 message = _("HTTP Error") def __init__(self, message=None, details=None, response=None, request_id=None, url=None, method=None, http_status=None): self.http_status = http_status or self.http_status self.message = message or self.message self.details = details self.request_id = request_id self.response = response self.url = url self.method = method formatted_string = "%s (HTTP %s)" % (self.message, self.http_status) if request_id: formatted_string += " (Request-ID: %s)" % request_id super(HttpError, self).__init__(formatted_string)
def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) baremetal_client = self.app.client_manager.baremetal if parsed_args.boot_index is not None and parsed_args.boot_index < 0: raise exc.CommandError( _('Expected non-negative --boot-index, got %s') % parsed_args.boot_index) properties = [] if parsed_args.node_uuid: properties.extend( utils.args_array_to_patch( 'add', ["node_uuid=%s" % parsed_args.node_uuid])) if parsed_args.volume_type: properties.extend( utils.args_array_to_patch( 'add', ["volume_type=%s" % parsed_args.volume_type])) if parsed_args.boot_index: properties.extend( utils.args_array_to_patch( 'add', ["boot_index=%s" % parsed_args.boot_index])) if parsed_args.volume_id: properties.extend( utils.args_array_to_patch( 'add', ["volume_id=%s" % parsed_args.volume_id])) if parsed_args.properties: properties.extend( utils.args_array_to_patch( 'add', ["properties/" + x for x in parsed_args.properties])) if parsed_args.extra: properties.extend( utils.args_array_to_patch( 'add', ["extra/" + x for x in parsed_args.extra])) if properties: baremetal_client.volume_target.update(parsed_args.volume_target, properties) else: self.log.warning("Please specify what to set.")
def split_and_deserialize(string): """Split and try to JSON deserialize a string. Gets a string with the KEY=VALUE format, split it (using '=' as the separator) and try to JSON deserialize the VALUE. :returns: A tuple of (key, value). """ try: key, value = string.split("=", 1) except ValueError: raise exc.CommandError(_('Attributes must be a list of ' 'PATH=VALUE not "%s"') % string) try: value = json.loads(value) except ValueError: pass return (key, value)
def __init__(self, os_ironic_api_version, api_version_select_state, max_retries, retry_interval, **kwargs): self.os_ironic_api_version = os_ironic_api_version self.api_version_select_state = api_version_select_state self.conflict_max_retries = max_retries self.conflict_retry_interval = retry_interval if isinstance(kwargs.get('endpoint_override'), six.string_types): kwargs['endpoint_override'] = _trim_endpoint_api_version( kwargs['endpoint_override']) super(SessionClient, self).__init__(**kwargs) endpoint_filter = self._get_endpoint_filter() endpoint = self.get_endpoint(**endpoint_filter) if endpoint is None: raise exc.EndpointNotFound( _('The Bare Metal API endpoint cannot be detected and was ' 'not provided explicitly')) self.endpoint_trimmed = _trim_endpoint_api_version(endpoint)
def print_dict(dct, dict_property="Property", wrap=0, dict_value='Value', json_flag=False): """Print a `dict` as a table of two columns. :param dct: `dict` to print :param dict_property: name of the first column :param wrap: wrapping for the second column :param dict_value: header label for the value (second) column :param json_flag: print `dict` as JSON instead of table """ if json_flag: print(json.dumps(dct, indent=4, separators=(',', ': '))) return pt = prettytable.PrettyTable([dict_property, dict_value]) pt.align = 'l' for k, v in sorted(dct.items()): # convert dict to str to check length if isinstance(v, dict): v = six.text_type(v) if wrap > 0: v = textwrap.fill(six.text_type(v), wrap) elif wrap < 0: raise ValueError(_("wrap argument should be a non-negative " "integer")) # if value has a newline, add in multiple rows # e.g. fault with stacktrace if v and isinstance(v, six.string_types) and r'\n' in v: lines = v.strip().split(r'\n') col1 = k for line in lines: pt.add_row([col1, line]) col1 = '' else: pt.add_row([k, v]) if six.PY3: print(encodeutils.safe_encode(pt.get_string()).decode()) else: print(encodeutils.safe_encode(pt.get_string()))
def _get_keystone_auth(self, session, auth_url, **kwargs): # FIXME(dhu): this code should come from keystoneclient # discover the supported keystone versions using the given url (v2_auth_url, v3_auth_url) = self._discover_auth_versions( session=session, auth_url=auth_url) # Determine which authentication plugin to use. First inspect the # auth_url to see the supported version. If both v3 and v2 are # supported, then use the highest version if possible. auth = None if v3_auth_url and v2_auth_url: user_domain_name = kwargs.get('user_domain_name', None) user_domain_id = kwargs.get('user_domain_id', None) project_domain_name = kwargs.get('project_domain_name', None) project_domain_id = kwargs.get('project_domain_id', None) # support both v2 and v3 auth. Use v3 if domain information is # provided. if (user_domain_name or user_domain_id or project_domain_name or project_domain_id): auth = self._get_keystone_v3_auth(v3_auth_url, **kwargs) else: auth = self._get_keystone_v2_auth(v2_auth_url, **kwargs) elif v3_auth_url: # support only v3 auth = self._get_keystone_v3_auth(v3_auth_url, **kwargs) elif v2_auth_url: # support only v2 auth = self._get_keystone_v2_auth(v2_auth_url, **kwargs) else: msg = _('Unable to determine the Keystone version ' 'to authenticate with using the given ' 'auth_url.') raise exc.CommandError(msg) return auth
def take_action(self, parsed_args): self.log.debug("take_action(%s)" % parsed_args) baremetal_client = self.app.client_manager.baremetal if parsed_args.boot_index < 0: raise exc.CommandError( _('Expected non-negative --boot-index, got %s') % parsed_args.boot_index) field_list = [ 'extra', 'volume_type', 'properties', 'boot_index', 'node_uuid', 'volume_id', 'uuid' ] fields = dict((k, v) for (k, v) in vars(parsed_args).items() if k in field_list and v is not None) fields = utils.args_array_to_dict(fields, 'properties') fields = utils.args_array_to_dict(fields, 'extra') volume_target = baremetal_client.volume_target.create(**fields) data = dict([(f, getattr(volume_target, f, '')) for f in res_fields.VOLUME_TARGET_DETAILED_RESOURCE.fields ]) return self.dict2columns(data)
def __init__(self, field_ids, sort_excluded=None): """Create a Resource object :param field_ids: A list of strings that the Resource object will contain. Each string must match an existing key in FIELDS. :param sort_excluded: Optional. A list of strings that will not be used for sorting. Must be a subset of 'field_ids'. :raises: ValueError if sort_excluded contains value not in field_ids """ self._fields = tuple(field_ids) self._labels = tuple([self.FIELDS[x] for x in field_ids]) if sort_excluded is None: sort_excluded = [] not_existing = set(sort_excluded) - set(field_ids) if not_existing: raise ValueError( _("sort_excluded specified with value not contained in " "field_ids. Unknown value(s): %s") % ','.join(not_existing)) self._sort_fields = tuple( [x for x in field_ids if x not in sort_excluded]) self._sort_labels = tuple([self.FIELDS[x] for x in self._sort_fields])
def make_client(instance): """Returns a baremetal service client.""" try: baremetal_client = ironic_client.get_client_class( instance._api_version[API_NAME]) except Exception: msg = (_("Invalid %(api_name)s client version '%(ver)s'. Must be one " "of %(supported_ver)s") % { 'api_name': API_NAME, 'ver': instance._api_version[API_NAME], 'supported_ver': ", ".join(sorted(API_VERSIONS)) }) raise exceptions.UnsupportedVersion(msg) LOG.debug('Instantiating baremetal client: %s', baremetal_client) client = baremetal_client( os_ironic_api_version=instance._api_version[API_NAME], session=instance.session, region_name=instance._region_name, endpoint=instance.auth_ref.auth_url, ) return client
def get_connection_params(endpoint, **kwargs): parts = urlparse.urlparse(endpoint) path = _trim_endpoint_api_version(parts.path) _args = (parts.hostname, parts.port, path) _kwargs = { 'timeout': (float(kwargs.get('timeout')) if kwargs.get('timeout') else 600) } if parts.scheme == 'https': _class = VerifiedHTTPSConnection _kwargs['ca_file'] = kwargs.get('ca_file', None) _kwargs['cert_file'] = kwargs.get('cert_file', None) _kwargs['key_file'] = kwargs.get('key_file', None) _kwargs['insecure'] = kwargs.get('insecure', False) elif parts.scheme == 'http': _class = six.moves.http_client.HTTPConnection else: msg = _('Unsupported scheme: %s') % parts.scheme raise exc.EndpointException(msg) return (_class, _args, _kwargs)
def find(self, base_url=None, **kwargs): """Find a single item with attributes matching ``**kwargs``. :param base_url: if provided, the generated URL will be appended to it """ kwargs = self._filter_kwargs(kwargs) rl = self._list( '%(base_url)s%(query)s' % { 'base_url': self.build_url(base_url=base_url, **kwargs), 'query': '?%s' % urlparse.urlencode(kwargs) if kwargs else '', }, self.collection_key) num = len(rl) if num == 0: msg = _("No %(name)s matching %(args)s.") % { 'name': self.resource_class.__name__, 'args': kwargs } raise exceptions.NotFound(msg) elif num > 1: raise exceptions.NoUniqueMatch else: return rl[0]
def check_empty_arg(arg, arg_descriptor): if not arg.strip(): raise exc.CommandError(_('%(arg)s cannot be empty or only have blank' ' spaces') % {'arg': arg_descriptor})
def negotiate_version(self, conn, resp): """Negotiate the server version Assumption: Called after receiving a 406 error when doing a request. :param conn: A connection object :param resp: The response object from http request """ def _query_server(conn): if (self.os_ironic_api_version and not isinstance(self.os_ironic_api_version, list) and self.os_ironic_api_version != 'latest'): base_version = ("/v%s" % str(self.os_ironic_api_version).split('.')[0]) else: base_version = API_VERSION return self._make_simple_request(conn, 'GET', base_version) version_overridden = False if (resp and hasattr(resp, 'request') and hasattr(resp.request, 'headers')): orig_hdr = resp.request.headers # Get the version of the client's last request and fallback # to the default for things like unit tests to not cause # migraines. req_api_ver = orig_hdr.get('X-OpenStack-Ironic-API-Version', self.os_ironic_api_version) else: req_api_ver = self.os_ironic_api_version if (resp and req_api_ver != self.os_ironic_api_version and self.api_version_select_state == 'negotiated'): # If we have a non-standard api version on the request, # but we think we've negotiated, then the call was overriden. # We should report the error with the called version requested_version = req_api_ver # And then we shouldn't save the newly negotiated # version of this negotiation because we have been # overridden a request. version_overridden = True else: requested_version = self.os_ironic_api_version if not resp: resp = _query_server(conn) if self.api_version_select_state not in API_VERSION_SELECTED_STATES: raise RuntimeError( _('Error: self.api_version_select_state should be one of the ' 'values in: "%(valid)s" but had the value: "%(value)s"') % { 'valid': ', '.join(API_VERSION_SELECTED_STATES), 'value': self.api_version_select_state }) min_ver, max_ver = self._parse_version_headers(resp) # NOTE: servers before commit 32fb6e99 did not return version headers # on error, so we need to perform a GET to determine # the supported version range if not max_ver: LOG.debug('No version header in response, requesting from server') resp = _query_server(conn) min_ver, max_ver = self._parse_version_headers(resp) # Reset the maximum version that we permit if StrictVersion(max_ver) > StrictVersion(LATEST_VERSION): LOG.debug( "Remote API version %(max_ver)s is greater than the " "version supported by ironicclient. Maximum available " "version is %(client_ver)s", { 'max_ver': max_ver, 'client_ver': LATEST_VERSION }) max_ver = LATEST_VERSION # If the user requested an explicit version or we have negotiated a # version and still failing then error now. The server could # support the version requested but the requested operation may not # be supported by the requested version. # TODO(TheJulia): We should break this method into several parts, # such as a sanity check/error method. if ((self.api_version_select_state == 'user' and not self._must_negotiate_version()) or (self.api_version_select_state == 'negotiated' and version_overridden)): raise exc.UnsupportedVersion( textwrap.fill( _("Requested API version %(req)s is not supported by the " "server, client, or the requested operation is not " "supported by the requested version. " "Supported version range is %(min)s to " "%(max)s") % { 'req': requested_version, 'min': min_ver, 'max': max_ver })) if (self.api_version_select_state == 'negotiated'): raise exc.UnsupportedVersion( textwrap.fill( _("No API version was specified or the requested operation " "was not supported by the client's negotiated API version " "%(req)s. Supported version range is: %(min)s to %(max)s" ) % { 'req': requested_version, 'min': min_ver, 'max': max_ver })) if isinstance(requested_version, six.string_types): if requested_version == 'latest': negotiated_ver = max_ver else: negotiated_ver = str( min(StrictVersion(requested_version), StrictVersion(max_ver))) elif isinstance(requested_version, list): if 'latest' in requested_version: raise ValueError( textwrap.fill( _("The 'latest' API version can not be requested " "in a list of versions. Please explicitly request " "'latest' or request only versios between " "%(min)s to %(max)s") % { 'min': min_ver, 'max': max_ver })) versions = [] for version in requested_version: if min_ver <= StrictVersion(version) <= max_ver: versions.append(StrictVersion(version)) if versions: negotiated_ver = str(max(versions)) else: raise exc.UnsupportedVersion( textwrap.fill( _("Requested API version specified and the requested " "operation was not supported by the client's " "requested API version %(req)s. Supported " "version range is: %(min)s to %(max)s") % { 'req': requested_version, 'min': min_ver, 'max': max_ver })) else: raise ValueError( textwrap.fill( _("Requested API version %(req)s type is unsupported. " "Valid types are Strings such as '1.1', 'latest' " "or a list of string values representing API versions.") % {'req': requested_version})) if StrictVersion(negotiated_ver) < StrictVersion(min_ver): negotiated_ver = min_ver # server handles microversions, but doesn't support # the requested version, so try a negotiated version self.api_version_select_state = 'negotiated' self.os_ironic_api_version = negotiated_ver LOG.debug('Negotiated API version is %s', negotiated_ver) # Cache the negotiated version for this server # TODO(vdrok): get rid of self.endpoint attribute in Stein endpoint_override = (getattr(self, 'endpoint_override', None) or getattr(self, 'endpoint', None)) host, port = get_server(endpoint_override) filecache.save_data(host=host, port=port, data=negotiated_ver) return negotiated_ver
def _http_request(self, url, method, **kwargs): """Send an http request with the specified characteristics. Wrapper around request.Session.request to handle tasks such as setting headers and error handling. """ # NOTE(TheJulia): self.os_ironic_api_version is reset in # the self.negotiate_version() call if negotiation occurs. if self.os_ironic_api_version and self._must_negotiate_version(): self.negotiate_version(self.session, None) # Copy the kwargs so we can reuse the original in case of redirects kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) kwargs['headers'].setdefault('User-Agent', USER_AGENT) if self.os_ironic_api_version: kwargs['headers'].setdefault('X-OpenStack-Ironic-API-Version', self.os_ironic_api_version) if self.auth_token: kwargs['headers'].setdefault('X-Auth-Token', self.auth_token) self.log_curl_request(method, url, kwargs) # NOTE(aarefiev): This is for backwards compatibility, request # expected body in 'data' field, previously we used httplib, # which expected 'body' field. body = kwargs.pop('body', None) if body: kwargs['data'] = body conn_url = self._make_connection_url(url) try: resp = self.session.request(method, conn_url, **kwargs) # TODO(deva): implement graceful client downgrade when connecting # to servers that did not support microversions. Details here: # https://specs.openstack.org/openstack/ironic-specs/specs/kilo-implemented/api-microversions.html#use-case-3b-new-client-communicating-with-a-old-ironic-user-specified # noqa if resp.status_code == http_client.NOT_ACCEPTABLE: negotiated_ver = self.negotiate_version(self.session, resp) kwargs['headers']['X-OpenStack-Ironic-API-Version'] = ( negotiated_ver) return self._http_request(url, method, **kwargs) except requests.exceptions.RequestException as e: message = (_("Error has occurred while handling " "request for %(url)s: %(e)s") % dict(url=conn_url, e=e)) # NOTE(aarefiev): not valid request(invalid url, missing schema, # and so on), retrying is not needed. if isinstance(e, ValueError): raise exc.ValidationError(message) raise exc.ConnectionRefused(message) body_str = None if resp.headers.get('Content-Type') == 'application/octet-stream': body_iter = resp.iter_content(chunk_size=CHUNKSIZE) self.log_http_response(resp) else: # Read body into string if it isn't obviously image data body_str = resp.text self.log_http_response(resp, body_str) body_iter = six.StringIO(body_str) if resp.status_code >= http_client.BAD_REQUEST: error_json = _extract_error_json(body_str) raise exc.from_response(resp, error_json.get('error_message'), error_json.get('debuginfo'), method, url) elif resp.status_code in (http_client.MOVED_PERMANENTLY, http_client.FOUND, http_client.USE_PROXY): # Redirected. Reissue the request to the new location. return self._http_request(resp['location'], method, **kwargs) elif resp.status_code == http_client.MULTIPLE_CHOICES: raise exc.from_response(resp, method=method, url=url) return resp, body_iter
def negotiate_version(self, conn, resp): """Negotiate the server version Assumption: Called after receiving a 406 error when doing a request. param conn: A connection object param resp: The response object from http request """ if self.api_version_select_state not in API_VERSION_SELECTED_STATES: raise RuntimeError( _('Error: self.api_version_select_state should be one of the ' 'values in: "%(valid)s" but had the value: "%(value)s"') % {'valid': ', '.join(API_VERSION_SELECTED_STATES), 'value': self.api_version_select_state}) min_ver, max_ver = self._parse_version_headers(resp) # NOTE: servers before commit 32fb6e99 did not return version headers # on error, so we need to perform a GET to determine # the supported version range if not max_ver: LOG.debug('No version header in response, requesting from server') if self.os_ironic_api_version: base_version = ("/v%s" % str(self.os_ironic_api_version).split('.')[0]) else: base_version = API_VERSION resp = self._make_simple_request(conn, 'GET', base_version) min_ver, max_ver = self._parse_version_headers(resp) # If the user requested an explicit version or we have negotiated a # version and still failing then error now. The server could # support the version requested but the requested operation may not # be supported by the requested version. if self.api_version_select_state == 'user': raise exc.UnsupportedVersion(textwrap.fill( _("Requested API version %(req)s is not supported by the " "server or the requested operation is not supported by the " "requested version. Supported version range is %(min)s to " "%(max)s") % {'req': self.os_ironic_api_version, 'min': min_ver, 'max': max_ver})) if self.api_version_select_state == 'negotiated': raise exc.UnsupportedVersion(textwrap.fill( _("No API version was specified and the requested operation " "was not supported by the client's negotiated API version " "%(req)s. Supported version range is: %(min)s to %(max)s") % {'req': self.os_ironic_api_version, 'min': min_ver, 'max': max_ver})) negotiated_ver = str(min(StrictVersion(self.os_ironic_api_version), StrictVersion(max_ver))) if negotiated_ver < min_ver: negotiated_ver = min_ver # server handles microversions, but doesn't support # the requested version, so try a negotiated version self.api_version_select_state = 'negotiated' self.os_ironic_api_version = negotiated_ver LOG.debug('Negotiated API version is %s', negotiated_ver) # Cache the negotiated version for this server host, port = get_server(self.endpoint) filecache.save_data(host=host, port=port, data=negotiated_ver) return negotiated_ver
def __init__(self, missing): self.missing = missing msg = _("Missing arguments: %s") % ", ".join(missing) super(MissingArgs, self).__init__(msg)
def print_list(objs, fields, formatters=None, sortby_index=0, mixed_case_fields=None, field_labels=None, json_flag=False): """Print a list of objects or dict as a table, one row per object or dict. :param objs: iterable of :class:`Resource` :param fields: attributes that correspond to columns, in order :param formatters: `dict` of callables for field formatting :param sortby_index: index of the field for sorting table rows :param mixed_case_fields: fields corresponding to object attributes that have mixed case names (e.g., 'serverId') :param field_labels: Labels to use in the heading of the table, default to fields. :param json_flag: print the list as JSON instead of table """ def _get_name_and_data(field): if field in formatters: # The value of the field has to be modified. # For example, it can be used to add extra fields. return (field, formatters[field](o)) field_name = field.replace(' ', '_') if field not in mixed_case_fields: field_name = field.lower() if isinstance(o, dict): data = o.get(field_name, '') else: data = getattr(o, field_name, '') return (field_name, data) formatters = formatters or {} mixed_case_fields = mixed_case_fields or [] field_labels = field_labels or fields if len(field_labels) != len(fields): raise ValueError(_("Field labels list %(labels)s has different number " "of elements than fields list %(fields)s"), {'labels': field_labels, 'fields': fields}) if sortby_index is None: kwargs = {} else: kwargs = {'sortby': field_labels[sortby_index]} pt = prettytable.PrettyTable(field_labels) pt.align = 'l' json_array = [] for o in objs: row = [] for field in fields: row.append(_get_name_and_data(field)) if json_flag: json_array.append(dict(row)) else: pt.add_row([r[1] for r in row]) if json_flag: print(json.dumps(json_array, indent=4, separators=(',', ': '))) elif six.PY3: print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode()) else: print(encodeutils.safe_encode(pt.get_string(**kwargs)))
def wait_for_provision_state(self, node_ident, expected_state, timeout=0, poll_interval=_DEFAULT_POLL_INTERVAL, poll_delay_function=None, fail_on_unexpected_state=True): """Helper function to wait for a node to reach a given state. Polls Ironic API in a loop until node gets to a requested state. Fails in the following cases: * Timeout (if provided) is reached * Node's last_error gets set to a non-empty value * Unexpected stable state is reached and fail_on_unexpected_state is on * Error state is reached (if it's not equal to expected_state) :param node_ident: node UUID or name :param expected_state: expected final provision state :param timeout: timeout in seconds, no timeout if 0 :param poll_interval: interval in seconds between 2 poll :param poll_delay_function: function to use to wait between polls (defaults to time.sleep). Should take one argument - delay time in seconds. Any exceptions raised inside it will abort the wait. :param fail_on_unexpected_state: whether to fail if the nodes reaches a different stable state. :raises: StateTransitionFailed if node reached an error state :raises: StateTransitionTimeout on timeout """ if not isinstance(timeout, (int, float)) or timeout < 0: raise ValueError(_('Timeout must be a non-negative number')) threshold = time.time() + timeout expected_state = expected_state.lower() poll_delay_function = (time.sleep if poll_delay_function is None else poll_delay_function) if not callable(poll_delay_function): raise TypeError(_('poll_delay_function must be callable')) # TODO(dtantsur): use version negotiation to request API 1.8 and use # the "fields" argument to reduce amount of data sent. while not timeout or time.time() < threshold: node = self.get(node_ident) if node.provision_state == expected_state: LOG.debug('Node %(node)s reached provision state %(state)s', { 'node': node_ident, 'state': expected_state }) return # Note that if expected_state == 'error' we still succeed if (node.last_error or node.provision_state == 'error' or node.provision_state.endswith(' failed')): raise exc.StateTransitionFailed( _('Node %(node)s failed to reach state %(state)s. ' 'It\'s in state %(actual)s, and has error: %(error)s') % { 'node': node_ident, 'state': expected_state, 'actual': node.provision_state, 'error': node.last_error }) if fail_on_unexpected_state and not node.target_provision_state: raise exc.StateTransitionFailed( _('Node %(node)s failed to reach state %(state)s. ' 'It\'s in unexpected stable state %(actual)s') % { 'node': node_ident, 'state': expected_state, 'actual': node.provision_state }) LOG.debug( 'Still waiting for node %(node)s to reach state ' '%(state)s, the current state is %(actual)s', { 'node': node_ident, 'state': expected_state, 'actual': node.provision_state }) poll_delay_function(poll_interval) raise exc.StateTransitionTimeout( _('Node %(node)s failed to reach state %(state)s in ' '%(timeout)s seconds') % { 'node': node_ident, 'state': expected_state, 'timeout': timeout })
def list(self, associated=None, maintenance=None, marker=None, limit=None, detail=False, sort_key=None, sort_dir=None, fields=None, provision_state=None, driver=None, resource_class=None, chassis=None): """Retrieve a list of nodes. :param associated: Optional. Either a Boolean or a string representation of a Boolean that indicates whether to return a list of associated (True or "True") or unassociated (False or "False") nodes. :param maintenance: Optional. Either a Boolean or a string representation of a Boolean that indicates whether to return nodes in maintenance mode (True or "True"), or not in maintenance mode (False or "False"). :param provision_state: Optional. String value to get only nodes in that provision state. :param marker: Optional, the UUID of a node, eg the last node from a previous result set. Return the next result set. :param limit: The maximum number of results to return per request, if: 1) limit > 0, the maximum number of nodes to return. 2) limit == 0, return the entire list of nodes. 3) limit param is NOT specified (None), the number of items returned respect the maximum imposed by the Ironic API (see Ironic's api.max_limit option). :param detail: Optional, boolean whether to return detailed information about nodes. :param sort_key: Optional, field used for sorting. :param sort_dir: Optional, direction of sorting, either 'asc' (the default) or 'desc'. :param fields: Optional, a list with a specified set of fields of the resource to be returned. Can not be used when 'detail' is set. :param driver: Optional. String value to get only nodes using that driver. :param resource_class: Optional. String value to get only nodes with the given resource class set. :param chassis: Optional, the UUID of a chassis. Used to get only nodes of this chassis. :returns: A list of nodes. """ if limit is not None: limit = int(limit) if detail and fields: raise exc.InvalidAttribute( _("Can't fetch a subset of fields " "with 'detail' set")) filters = utils.common_filters(marker, limit, sort_key, sort_dir, fields) if associated is not None: filters.append('associated=%s' % associated) if maintenance is not None: filters.append('maintenance=%s' % maintenance) if provision_state is not None: filters.append('provision_state=%s' % provision_state) if driver is not None: filters.append('driver=%s' % driver) if resource_class is not None: filters.append('resource_class=%s' % resource_class) if chassis is not None: filters.append('chassis_uuid=%s' % chassis) path = '' if detail: path += 'detail' if filters: path += '?' + '&'.join(filters) if limit is None: return self._list(self._path(path), "nodes") else: return self._list_pagination(self._path(path), "nodes", limit=limit)
def list_volume_targets(self, node_id, marker=None, limit=None, sort_key=None, sort_dir=None, detail=False, fields=None): """List all the volume targets for a given node. :param node_id: Name or UUID of the node. :param marker: Optional, the UUID of a volume target, eg the last volume target from a previous result set. Return the next result set. :param limit: The maximum number of results to return per request, if: 1) limit > 0, the maximum number of volume targets to return. 2) limit == 0, return the entire list of volume targets. 3) limit param is NOT specified (None), the number of items returned respect the maximum imposed by the Ironic API (see Ironic's api.max_limit option). :param sort_key: Optional, field used for sorting. :param sort_dir: Optional, direction of sorting, either 'asc' (the default) or 'desc'. :param detail: Optional, boolean whether to return detailed information about volume targets. :param fields: Optional, a list with a specified set of fields of the resource to be returned. Can not be used when 'detail' is set. :returns: A list of volume targets. """ if limit is not None: limit = int(limit) if detail and fields: raise exc.InvalidAttribute( _("Can't fetch a subset of fields " "with 'detail' set")) filters = utils.common_filters(marker=marker, limit=limit, sort_key=sort_key, sort_dir=sort_dir, fields=fields, detail=detail) path = "%s/volume/targets" % node_id if filters: path += '?' + '&'.join(filters) if limit is None: return self._list(self._path(path), response_key="targets", obj_class=volume_target.VolumeTarget) else: return self._list_pagination(self._path(path), response_key="targets", limit=limit, obj_class=volume_target.VolumeTarget)