Hello community,

here is the log from the commit of package python-ironicclient for 
openSUSE:Factory checked in at 2018-03-19 23:34:40
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-ironicclient (Old)
 and      /work/SRC/openSUSE:Factory/.python-ironicclient.new (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-ironicclient"

Mon Mar 19 23:34:40 2018 rev:12 rq:583301 version:2.2.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-ironicclient/python-ironicclient.changes  
2018-01-31 19:52:04.552963268 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-ironicclient.new/python-ironicclient.changes 
    2018-03-19 23:34:42.441298773 +0100
@@ -1,0 +2,19 @@
+Fri Feb 23 13:03:45 UTC 2018 - [email protected]
+
+- Switch to stable/queens spec template
+
+-------------------------------------------------------------------
+Mon Feb 12 09:56:58 UTC 2018 - [email protected]
+
+- update to version 2.2.0 (bsc#1078607)
+  - Use StrictVersion to compare versions
+  - Facilitate latest Rest API use
+  - Allow API user to define list of versions
+  - Ignore .eggs from git
+  - Traits support
+  - Add release note for fix to bug 1745099
+  - Can not set portgroup mode as a number
+  - Accept port and portgroup as volume connector types
+  - Updated from global requirements
+
+-------------------------------------------------------------------

Old:
----
  python-ironicclient-2.1.0.tar.gz

New:
----
  python-ironicclient-2.2.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-ironicclient.spec ++++++
--- /var/tmp/diff_new_pack.8yVrkH/_old  2018-03-19 23:34:43.133273810 +0100
+++ /var/tmp/diff_new_pack.8yVrkH/_new  2018-03-19 23:34:43.137273666 +0100
@@ -16,50 +16,78 @@
 #
 
 
-%global sname python-ironicclient
 Name:           python-ironicclient
-Version:        2.1.0
+Version:        2.2.0
 Release:        0
 Summary:        Python API and CLI for OpenStack Ironic
 License:        Apache-2.0
 Group:          Development/Languages/Python
-Url:            https://launchpad.net/%{sname}
-Source0:        
https://files.pythonhosted.org/packages/source/p/%{sname}/%{sname}-%{version}.tar.gz
+Url:            https://launchpad.net/python-ironicclient
+Source0:        
https://files.pythonhosted.org/packages/source/p/python-ironicclient/python-ironicclient-2.2.0.tar.gz
 BuildRequires:  openstack-macros
-BuildRequires:  python-Babel >= 2.3.4
-BuildRequires:  python-PrettyTable >= 0.7.1
-BuildRequires:  python-PyYAML >= 3.10
-BuildRequires:  python-appdirs >= 1.3.0
 BuildRequires:  python-devel
-BuildRequires:  python-dogpile.cache >= 0.6.2
-BuildRequires:  python-fixtures >= 3.0.0
-BuildRequires:  python-jsonschema >= 2.6.0
-BuildRequires:  python-mock >= 2.0.0
-BuildRequires:  python-openstackclient >= 3.12.0
-BuildRequires:  python-os-testr >= 1.0.0
-BuildRequires:  python-osc-lib >= 1.7.0
-BuildRequires:  python-oslo.i18n >= 3.15.3
-BuildRequires:  python-oslo.utils >= 3.31.0
-BuildRequires:  python-oslotest >= 1.10.0
-BuildRequires:  python-pbr >= 2.0.0
-BuildRequires:  python-python-subunit >= 1.0.0
-BuildRequires:  python-requests >= 2.14.2
-BuildRequires:  python-requests-mock >= 1.1.0
-BuildRequires:  python-testtools >= 2.2.0
+BuildRequires:  python2-Babel >= 2.3.4
+BuildRequires:  python2-PrettyTable >= 0.7.1
+BuildRequires:  python2-PyYAML >= 3.10
+BuildRequires:  python2-appdirs >= 1.3.0
+BuildRequires:  python2-dogpile.cache >= 0.6.2
+BuildRequires:  python2-fixtures >= 3.0.0
+BuildRequires:  python2-jsonschema >= 2.6.0
+BuildRequires:  python2-mock >= 2.0.0
+BuildRequires:  python2-openstackclient >= 3.12.0
+BuildRequires:  python2-os-testr >= 1.0.0
+BuildRequires:  python2-osc-lib >= 1.8.0
+BuildRequires:  python2-oslo.i18n >= 3.15.3
+BuildRequires:  python2-oslo.utils >= 3.33.0
+BuildRequires:  python2-oslotest >= 3.2.0
+BuildRequires:  python2-pbr >= 2.0.0
+BuildRequires:  python2-python-subunit >= 1.0.0
+BuildRequires:  python2-requests >= 2.14.2
+BuildRequires:  python2-requests-mock >= 1.1.0
+BuildRequires:  python2-testtools >= 2.2.0
+BuildRequires:  python3-Babel >= 2.3.4
+BuildRequires:  python3-PrettyTable >= 0.7.1
+BuildRequires:  python3-PyYAML >= 3.10
+BuildRequires:  python3-appdirs >= 1.3.0
+BuildRequires:  python3-devel
+BuildRequires:  python3-dogpile.cache >= 0.6.2
+BuildRequires:  python3-fixtures >= 3.0.0
+BuildRequires:  python3-jsonschema >= 2.6.0
+BuildRequires:  python3-mock >= 2.0.0
+BuildRequires:  python3-openstackclient >= 3.12.0
+BuildRequires:  python3-os-testr >= 1.0.0
+BuildRequires:  python3-osc-lib >= 1.8.0
+BuildRequires:  python3-oslo.i18n >= 3.15.3
+BuildRequires:  python3-oslo.utils >= 3.33.0
+BuildRequires:  python3-oslotest >= 3.2.0
+BuildRequires:  python3-pbr >= 2.0.0
+BuildRequires:  python3-python-subunit >= 1.0.0
+BuildRequires:  python3-requests >= 2.14.2
+BuildRequires:  python3-requests-mock >= 1.1.0
+BuildRequires:  python3-testtools >= 2.2.0
 Requires:       python-PrettyTable >= 0.7.1
 Requires:       python-PyYAML >= 3.10
 Requires:       python-appdirs >= 1.3.0
 Requires:       python-dogpile.cache >= 0.6.2
 Requires:       python-jsonschema >= 2.6.0
-Requires:       python-keystoneauth1 >= 3.2.0
+Requires:       python-keystoneauth1 >= 3.3.0
 Requires:       python-openstackclient >= 3.12.0
-Requires:       python-osc-lib >= 1.7.0
+Requires:       python-osc-lib >= 1.8.0
 Requires:       python-oslo.i18n >= 3.15.3
-Requires:       python-oslo.utils >= 3.31.0
+Requires:       python-oslo.utils >= 3.33.0
 Requires:       python-pbr >= 2.0.0
 Requires:       python-requests >= 2.14.2
 Requires:       python-six >= 1.10.0
 BuildArch:      noarch
+%if 0%{?suse_version}
+Requires(post): update-alternatives
+Requires(postun): update-alternatives
+%else
+# on RDO, update-alternatives is in chkconfig
+Requires(post): chkconfig
+Requires(postun): chkconfig
+%endif
+%python_subpackages
 
 %description
 OpenStack Bare Metal Provisioning API Client Library
@@ -67,49 +95,58 @@
 This is a client for the OpenStack Ironic API. It provides a Python API (the
 ironicclient module) and a command-line interface (ironic).
 
-%package doc
+%package -n python-ironicclient-doc
 Summary:        Documentation for OpenStack Ironic API Client
 Group:          Documentation/HTML
 BuildRequires:  python-Sphinx
-BuildRequires:  python-openstackdocstheme >= 1.17.0
+BuildRequires:  python-openstackdocstheme >= 1.18.1
 BuildRequires:  python-reno >= 2.5.0
 
-%description doc
+%description -n python-ironicclient-doc
 This is a client for the OpenStack Ironic API (Bare Metal. There's a
 Python API (the ironicclient module), and a command-line script (ironic).
 Each implements 100% of the OpenStack Ironic API.
 This package contains auto-generated documentation.
 
 %prep
-%autosetup -n %{sname}-%{version}
+%autosetup -p1 -n python-ironicclient-2.2.0
 %py_req_cleanup
 sed -i 's/^warning-is-error.*/warning-is-error = 0/g' setup.cfg
 
 %build
-%{py2_build}
+%{python_build}
 
 %{__python2} setup.py build_sphinx
 # remove the sphinx-build leftovers
 rm -rf doc/build/html/.{doctrees,buildinfo}
 
 %install
-%{py2_install}
+%{python_install}
 # bash completion
 install -p -D -m 644 tools/ironic.bash_completion 
%{buildroot}%{_sysconfdir}/bash_completion.d/ironic.bash_completion
+%python_clone -a %{buildroot}%{_bindir}/ironic
+%python_clone -a 
%{buildroot}%{_sysconfdir}/bash_completion.d/ironic.bash_completion
+
+%post
+%{python_install_alternative ironic 
%{_sysconfdir}/bash_completion.d/ironic.bash_completion}
+
+%postun
+%python_uninstall_alternative ironic
 
 %check
+%{python_expand rm -rf .testrepository
 ostestr
+}
 
-%files
+%files %{python_files}
 %license LICENSE
 %doc README.rst
-%{_bindir}/ironic
-%{python2_sitelib}/ironicclient
-%{python2_sitelib}/*.egg-info
-%{_bindir}/ironic
-%{_sysconfdir}/bash_completion.d/ironic.bash_completion
+%{python_sitelib}/ironicclient
+%{python_sitelib}/*.egg-info
+%python_alternative %{_bindir}/ironic
+%python_alternative %{_sysconfdir}/bash_completion.d/ironic.bash_completion
 
-%files doc
+%files -n python-ironicclient-doc
 %license LICENSE
 %doc doc/build/html
 

++++++ _service ++++++
--- /var/tmp/diff_new_pack.8yVrkH/_old  2018-03-19 23:34:43.181272079 +0100
+++ /var/tmp/diff_new_pack.8yVrkH/_new  2018-03-19 23:34:43.185271935 +0100
@@ -1,8 +1,8 @@
 <services>
   <service mode="disabled" name="renderspec">
-    <param 
name="input-template">https://git.openstack.org/cgit/openstack/rpm-packaging/plain/openstack/python-ironicclient/python-ironicclient.spec.j2?h=master</param>
+    <param 
name="input-template">https://git.openstack.org/cgit/openstack/rpm-packaging/plain/openstack/python-ironicclient/python-ironicclient.spec.j2?h=stable/queens</param>
     <param name="output-name">python-ironicclient.spec</param>
-    <param 
name="requirements">https://raw.githubusercontent.com/openstack/rpm-packaging/master/requirements.txt</param>
+    <param 
name="requirements">https://raw.githubusercontent.com/openstack/rpm-packaging/stable/queens/requirements.txt</param>
     <param name="changelog-email">[email protected]</param>
     <param name="changelog-provider">gh,openstack,python-ironicclient</param>
   </service>

++++++ python-ironicclient-2.1.0.tar.gz -> python-ironicclient-2.2.0.tar.gz 
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-ironicclient-2.1.0/AUTHORS 
new/python-ironicclient-2.2.0/AUTHORS
--- old/python-ironicclient-2.1.0/AUTHORS       2018-01-08 14:52:17.000000000 
+0100
+++ new/python-ironicclient-2.2.0/AUTHORS       2018-01-26 01:44:32.000000000 
+0100
@@ -47,6 +47,7 @@
 Julia Kreger <[email protected]>
 KATO Tomoyuki <[email protected]>
 KaiFeng Wang <[email protected]>
+Kaifeng Wang <[email protected]>
 Kan <[email protected]>
 Kevin McDonald <[email protected]>
 Kui Shi <[email protected]>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-ironicclient-2.1.0/ChangeLog 
new/python-ironicclient-2.2.0/ChangeLog
--- old/python-ironicclient-2.1.0/ChangeLog     2018-01-08 14:52:16.000000000 
+0100
+++ new/python-ironicclient-2.2.0/ChangeLog     2018-01-26 01:44:32.000000000 
+0100
@@ -1,6 +1,22 @@
 CHANGES
 =======
 
+2.2.0
+-----
+
+* Add release note for fix to bug 1745099
+* Traits support
+* Can not set portgroup mode as a number
+* Updated from global requirements
+* Allow API user to define list of versions
+* Facilitate latest Rest API use
+* Updated from global requirements
+* Updated from global requirements
+* Updated from global requirements
+* Use StrictVersion to compare versions
+* Accept port and portgroup as volume connector types
+* Ignore .eggs from git
+
 2.1.0
 -----
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-ironicclient-2.1.0/PKG-INFO 
new/python-ironicclient-2.2.0/PKG-INFO
--- old/python-ironicclient-2.1.0/PKG-INFO      2018-01-08 14:52:18.000000000 
+0100
+++ new/python-ironicclient-2.2.0/PKG-INFO      2018-01-26 01:44:33.000000000 
+0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: python-ironicclient
-Version: 2.1.0
+Version: 2.2.0
 Summary: OpenStack Bare Metal Provisioning API Client Library
 Home-page: https://docs.openstack.org/python-ironicclient/latest/
 Author: OpenStack
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-ironicclient-2.1.0/ironicclient/client.py 
new/python-ironicclient-2.2.0/ironicclient/client.py
--- old/python-ironicclient-2.1.0/ironicclient/client.py        2018-01-08 
14:49:17.000000000 +0100
+++ new/python-ironicclient-2.2.0/ironicclient/client.py        2018-01-26 
01:40:03.000000000 +0100
@@ -58,7 +58,8 @@
     :param cert_file: path to cert file, deprecated in favour of os_cert
     :param os_key: path to key file
     :param key_file: path to key file, deprecated in favour of os_key
-    :param os_ironic_api_version: ironic API version to use
+    :param os_ironic_api_version: ironic API version to use or a list of
+        available API versions to attempt to negotiate.
     :param max_retries: Maximum number of retries in case of conflict error
     :param retry_interval: Amount of time (in seconds) between retries in case
         of conflict error
@@ -66,6 +67,11 @@
     :param ignored_kwargs: all the other params that are passed. Left for
         backwards compatibility. They are ignored.
     """
+    # TODO(TheJulia): At some point, we should consider possibly noting
+    # the "latest" flag for os_ironic_api_version to cause the client to
+    # auto-negotiate to the greatest available version, however we do not
+    # have the ability yet for a caller to cap the version, and will hold
+    # off doing so until then.
     os_service_type = os_service_type or 'baremetal'
     os_endpoint_type = os_endpoint_type or 'publicURL'
     project_id = (os_project_id or os_tenant_id)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/ironicclient/common/base.py 
new/python-ironicclient-2.2.0/ironicclient/common/base.py
--- old/python-ironicclient-2.1.0/ironicclient/common/base.py   2018-01-08 
14:49:17.000000000 +0100
+++ new/python-ironicclient-2.2.0/ironicclient/common/base.py   2018-01-26 
01:40:27.000000000 +0100
@@ -170,25 +170,34 @@
 
         return object_list
 
-    def _list(self, url, response_key=None, obj_class=None, body=None):
+    def __list(self, url, response_key=None, body=None):
         resp, body = self.api.json_request('GET', url)
+        data = self._format_body_data(body, response_key)
+        return data
 
+    def _list(self, url, response_key=None, obj_class=None, body=None):
         if obj_class is None:
             obj_class = self.resource_class
 
-        data = self._format_body_data(body, response_key)
+        data = self.__list(url, response_key=response_key, body=body)
         return [obj_class(self, res, loaded=True) for res in data if res]
 
+    def _list_primitives(self, url, response_key=None):
+        return self.__list(url, response_key=response_key)
+
     def _update(self, resource_id, patch, method='PATCH'):
         """Update a resource.
 
         :param resource_id: Resource identifier.
-        :param patch: New version of a given resource.
+        :param patch: New version of a given resource, a dictionary or None.
         :param method: Name of the method for the request.
         """
 
         url = self._path(resource_id)
-        resp, body = self.api.json_request(method, url, body=patch)
+        kwargs = {}
+        if patch is not None:
+            kwargs['body'] = patch
+        resp, body = self.api.json_request(method, url, **kwargs)
         # PATCH/PUT requests may not return a body
         if body:
             return self.resource_class(self, body)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/ironicclient/common/http.py 
new/python-ironicclient-2.2.0/ironicclient/common/http.py
--- old/python-ironicclient-2.1.0/ironicclient/common/http.py   2018-01-08 
14:49:17.000000000 +0100
+++ new/python-ironicclient-2.2.0/ironicclient/common/http.py   2018-01-26 
01:40:27.000000000 +0100
@@ -44,7 +44,8 @@
 #             
http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html
 # noqa
 #             for full details.
 DEFAULT_VER = '1.9'
-
+LAST_KNOWN_API_VERSION = 37
+LATEST_VERSION = '1.{}'.format(LAST_KNOWN_API_VERSION)
 
 LOG = logging.getLogger(__name__)
 USER_AGENT = 'python-ironicclient'
@@ -98,6 +99,18 @@
         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)
+
+        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 '
@@ -110,22 +123,31 @@
         # 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)
+            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.
-        if self.api_version_select_state == 'user':
+        # 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
+                self.os_ironic_api_version != 'latest' and
+                not isinstance(self.os_ironic_api_version, list)):
             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 "
+                  "server, client, 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}))
@@ -137,9 +159,46 @@
                 % {'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:
+        if isinstance(self.os_ironic_api_version, six.string_types):
+            if self.os_ironic_api_version == 'latest':
+                negotiated_ver = max_ver
+            else:
+                negotiated_ver = str(
+                    min(StrictVersion(self.os_ironic_api_version),
+                        StrictVersion(max_ver)))
+
+        elif isinstance(self.os_ironic_api_version, list):
+            if 'latest' in self.os_ironic_api_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 self.os_ironic_api_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': self.os_ironic_api_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': self.os_ironic_api_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
@@ -310,6 +369,14 @@
         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.api_version_select_state == 'user' and
+                (self.os_ironic_api_version == 'latest' or
+                 isinstance(self.os_ironic_api_version, list))):
+            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)
@@ -517,6 +584,15 @@
 
     @with_retries
     def _http_request(self, url, method, **kwargs):
+
+        # 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.api_version_select_state == 'user' and
+                (self.os_ironic_api_version == 'latest' or
+                 isinstance(self.os_ironic_api_version, list))):
+            self.negotiate_version(self.session, None)
+
         kwargs.setdefault('user_agent', USER_AGENT)
         kwargs.setdefault('auth', self.auth)
         if isinstance(self.endpoint_override, six.string_types):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-ironicclient-2.1.0/ironicclient/osc/plugin.py 
new/python-ironicclient-2.2.0/ironicclient/osc/plugin.py
--- old/python-ironicclient-2.1.0/ironicclient/osc/plugin.py    2018-01-08 
14:49:17.000000000 +0100
+++ new/python-ironicclient-2.2.0/ironicclient/osc/plugin.py    2018-01-26 
01:40:03.000000000 +0100
@@ -19,6 +19,7 @@
 import argparse
 import logging
 
+from ironicclient.common import http
 from osc_lib import utils
 
 LOG = logging.getLogger(__name__)
@@ -26,8 +27,13 @@
 CLIENT_CLASS = 'ironicclient.v1.client.Client'
 API_VERSION_OPTION = 'os_baremetal_api_version'
 API_NAME = 'baremetal'
-LAST_KNOWN_API_VERSION = 35
-LATEST_VERSION = "1.{}".format(LAST_KNOWN_API_VERSION)
+# NOTE(TheJulia) Latest known version tracking has been moved
+# to the ironicclient/common/http.py file as the OSC committment
+# is latest known, and we should only store it in one location.
+LAST_KNOWN_API_VERSION = http.LAST_KNOWN_API_VERSION
+LATEST_VERSION = http.LATEST_VERSION
+
+
 API_VERSIONS = {
     '1.%d' % i: CLIENT_CLASS
     for i in range(1, LAST_KNOWN_API_VERSION + 1)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/ironicclient/osc/v1/baremetal_node.py 
new/python-ironicclient-2.2.0/ironicclient/osc/v1/baremetal_node.py
--- old/python-ironicclient-2.1.0/ironicclient/osc/v1/baremetal_node.py 
2018-01-08 14:49:17.000000000 +0100
+++ new/python-ironicclient-2.2.0/ironicclient/osc/v1/baremetal_node.py 
2018-01-26 01:40:27.000000000 +0100
@@ -1574,3 +1574,116 @@
         baremetal_client = self.app.client_manager.baremetal
 
         baremetal_client.node.inject_nmi(parsed_args.node)
+
+
+class ListTraitsBaremetalNode(command.Lister):
+    """List a node's traits."""
+
+    log = logging.getLogger(__name__ + ".ListTraitsBaremetalNode")
+
+    def get_parser(self, prog_name):
+        parser = super(ListTraitsBaremetalNode, self).get_parser(prog_name)
+
+        parser.add_argument(
+            'node',
+            metavar='<node>',
+            help=_("Name or UUID of the node"))
+
+        return parser
+
+    def take_action(self, parsed_args):
+        self.log.debug("take_action(%s)", parsed_args)
+
+        labels = res_fields.TRAIT_RESOURCE.labels
+
+        baremetal_client = self.app.client_manager.baremetal
+        traits = baremetal_client.node.get_traits(parsed_args.node)
+
+        return (labels, [[trait] for trait in traits])
+
+
+class AddTraitBaremetalNode(command.Command):
+    """Add traits to a node."""
+
+    log = logging.getLogger(__name__ + ".AddTraitBaremetalNode")
+
+    def get_parser(self, prog_name):
+        parser = super(AddTraitBaremetalNode, self).get_parser(prog_name)
+
+        parser.add_argument(
+            'node',
+            metavar='<node>',
+            help=_("Name or UUID of the node"))
+        parser.add_argument(
+            'traits',
+            nargs='+',
+            metavar='<trait>',
+            help=_("Trait(s) to add"))
+
+        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 trait in parsed_args.traits:
+            try:
+                baremetal_client.node.add_trait(parsed_args.node, trait)
+                print(_('Added trait %s') % trait)
+            except exc.ClientException as e:
+                failures.append(_("Failed to add trait %(trait)s: %(error)s")
+                                % {'trait': trait, 'error': e})
+
+        if failures:
+            raise exc.ClientException("\n".join(failures))
+
+
+class RemoveTraitBaremetalNode(command.Command):
+    """Remove trait(s) from a node."""
+
+    log = logging.getLogger(__name__ + ".RemoveTraitBaremetalNode")
+
+    def get_parser(self, prog_name):
+        parser = super(RemoveTraitBaremetalNode, self).get_parser(prog_name)
+
+        parser.add_argument(
+            'node',
+            metavar='<node>',
+            help=_("Name or UUID of the node"))
+        all_or_trait = parser.add_mutually_exclusive_group(required=True)
+        all_or_trait.add_argument(
+            '--all',
+            dest='remove_all',
+            action='store_true',
+            help=_("Remove all traits"))
+        all_or_trait.add_argument(
+            'traits',
+            metavar='<trait>',
+            nargs='*',
+            default=[],
+            help=_("Trait(s) to remove"))
+
+        return parser
+
+    def take_action(self, parsed_args):
+        self.log.debug("take_action(%s)", parsed_args)
+
+        baremetal_client = self.app.client_manager.baremetal
+
+        failures = []
+        if parsed_args.remove_all:
+            baremetal_client.node.remove_all_traits(parsed_args.node)
+        else:
+            for trait in parsed_args.traits:
+                try:
+                    baremetal_client.node.remove_trait(parsed_args.node, trait)
+                    print(_('Removed trait %s') % trait)
+                except exc.ClientException as e:
+                    failures.append(_("Failed to remove trait %(trait)s: "
+                                      "%(error)s")
+                                    % {'trait': trait, 'error': e})
+
+        if failures:
+            raise exc.ClientException("\n".join(failures))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/ironicclient/osc/v1/baremetal_portgroup.py 
new/python-ironicclient-2.2.0/ironicclient/osc/v1/baremetal_portgroup.py
--- old/python-ironicclient-2.1.0/ironicclient/osc/v1/baremetal_portgroup.py    
2018-01-08 14:49:17.000000000 +0100
+++ new/python-ironicclient-2.2.0/ironicclient/osc/v1/baremetal_portgroup.py    
2018-01-26 01:40:27.000000000 +0100
@@ -379,7 +379,7 @@
                 'add', ["standalone_ports_supported=False"]))
         if parsed_args.mode:
             properties.extend(utils.args_array_to_patch(
-                'add', ["mode=%s" % parsed_args.mode]))
+                'add', ["mode=\"%s\"" % parsed_args.mode]))
 
         if parsed_args.extra:
             properties.extend(utils.args_array_to_patch(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/ironicclient/osc/v1/baremetal_volume_connector.py 
new/python-ironicclient-2.2.0/ironicclient/osc/v1/baremetal_volume_connector.py
--- 
old/python-ironicclient-2.1.0/ironicclient/osc/v1/baremetal_volume_connector.py 
    2018-01-08 14:49:17.000000000 +0100
+++ 
new/python-ironicclient-2.2.0/ironicclient/osc/v1/baremetal_volume_connector.py 
    2018-01-26 01:40:27.000000000 +0100
@@ -45,9 +45,9 @@
             dest='type',
             metavar="<type>",
             required=True,
-            choices=('iqn', 'ip', 'mac', 'wwnn', 'wwpn'),
+            choices=('iqn', 'ip', 'mac', 'wwnn', 'wwpn', 'port', 'portgroup'),
             help=_("Type of the volume connector. Can be 'iqn', 'ip', 'mac', "
-                   "'wwnn', 'wwpn'."))
+                   "'wwnn', 'wwpn', 'port', 'portgroup'."))
         parser.add_argument(
             '--connector-id',
             dest='connector_id',
@@ -279,9 +279,9 @@
             '--type',
             dest='type',
             metavar="<type>",
-            choices=('iqn', 'ip', 'mac', 'wwnn', 'wwpn'),
+            choices=('iqn', 'ip', 'mac', 'wwnn', 'wwpn', 'port', 'portgroup'),
             help=_("Type of the volume connector. Can be 'iqn', 'ip', 'mac', "
-                   "'wwnn', 'wwpn'."))
+                   "'wwnn', 'wwpn', 'port', 'portgroup'."))
         parser.add_argument(
             '--connector-id',
             dest='connector_id',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/ironicclient/tests/unit/common/test_http.py 
new/python-ironicclient-2.2.0/ironicclient/tests/unit/common/test_http.py
--- old/python-ironicclient-2.1.0/ironicclient/tests/unit/common/test_http.py   
2018-01-08 14:49:17.000000000 +0100
+++ new/python-ironicclient-2.2.0/ironicclient/tests/unit/common/test_http.py   
2018-01-26 01:40:03.000000000 +0100
@@ -183,6 +183,141 @@
         self.assertEqual(1, mock_pvh.call_count)
         self.assertEqual(0, mock_save_data.call_count)
 
+    @mock.patch.object(filecache, 'save_data', autospec=True)
+    @mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers',
+                       autospec=True)
+    def test_negotiate_version_strict_version_comparison(self, mock_pvh,
+                                                         mock_save_data):
+        # Test version comparison with StrictVersion
+        max_ver = '1.10'
+        mock_pvh.return_value = ('1.2', max_ver)
+        mock_conn = mock.MagicMock()
+        self.test_object.os_ironic_api_version = '1.10'
+        result = self.test_object.negotiate_version(mock_conn, self.response)
+        self.assertEqual(max_ver, result)
+        self.assertEqual(1, mock_pvh.call_count)
+        host, port = http.get_server(self.test_object.endpoint)
+        mock_save_data.assert_called_once_with(host=host, port=port,
+                                               data=max_ver)
+
+    @mock.patch.object(filecache, 'save_data', autospec=True)
+    @mock.patch.object(http.VersionNegotiationMixin, '_make_simple_request',
+                       autospec=True)
+    @mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers',
+                       autospec=True)
+    def test_negotiate_version_server_user_latest(
+            self, mock_pvh, mock_msr, mock_save_data):
+        # have to retry with simple get
+        mock_pvh.side_effect = iter([(None, None), ('1.1', '1.99')])
+        mock_conn = mock.MagicMock()
+        self.test_object.api_version_select_state = 'user'
+        self.test_object.os_ironic_api_version = 'latest'
+        result = self.test_object.negotiate_version(mock_conn, None)
+        self.assertEqual(http.LATEST_VERSION, result)
+        self.assertEqual('negotiated',
+                         self.test_object.api_version_select_state)
+        self.assertEqual(http.LATEST_VERSION,
+                         self.test_object.os_ironic_api_version)
+
+        self.assertTrue(mock_msr.called)
+        self.assertEqual(2, mock_pvh.call_count)
+        self.assertEqual(1, mock_save_data.call_count)
+
+    @mock.patch.object(filecache, 'save_data', autospec=True)
+    @mock.patch.object(http.VersionNegotiationMixin, '_make_simple_request',
+                       autospec=True)
+    @mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers',
+                       autospec=True)
+    def test_negotiate_version_server_user_list(
+            self, mock_pvh, mock_msr, mock_save_data):
+        # have to retry with simple get
+        mock_pvh.side_effect = iter([(None, None), ('1.1', '1.26')])
+        mock_conn = mock.MagicMock()
+        self.test_object.api_version_select_state = 'user'
+        self.test_object.os_ironic_api_version = ['1.1', '1.6', '1.25',
+                                                  '1.26', '1.26.1', '1.27',
+                                                  '1.30']
+        result = self.test_object.negotiate_version(mock_conn, self.response)
+        self.assertEqual('1.26', result)
+        self.assertEqual('negotiated',
+                         self.test_object.api_version_select_state)
+        self.assertEqual('1.26',
+                         self.test_object.os_ironic_api_version)
+
+        self.assertTrue(mock_msr.called)
+        self.assertEqual(2, mock_pvh.call_count)
+        self.assertEqual(1, mock_save_data.call_count)
+
+    @mock.patch.object(filecache, 'save_data', autospec=True)
+    @mock.patch.object(http.VersionNegotiationMixin, '_make_simple_request',
+                       autospec=True)
+    @mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers',
+                       autospec=True)
+    def test_negotiate_version_server_user_list_fails_nomatch(
+            self, mock_pvh, mock_msr, mock_save_data):
+        # have to retry with simple get
+        mock_pvh.side_effect = iter([(None, None), ('1.2', '1.26')])
+        mock_conn = mock.MagicMock()
+        self.test_object.api_version_select_state = 'user'
+        self.test_object.os_ironic_api_version = ['1.39', '1.1']
+        self.assertRaises(
+            exc.UnsupportedVersion,
+            self.test_object.negotiate_version,
+            mock_conn, self.response)
+        self.assertEqual('user',
+                         self.test_object.api_version_select_state)
+        self.assertEqual(['1.39', '1.1'],
+                         self.test_object.os_ironic_api_version)
+        self.assertEqual(2, mock_pvh.call_count)
+        self.assertEqual(0, mock_save_data.call_count)
+
+    @mock.patch.object(filecache, 'save_data', autospec=True)
+    @mock.patch.object(http.VersionNegotiationMixin, '_make_simple_request',
+                       autospec=True)
+    @mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers',
+                       autospec=True)
+    def test_negotiate_version_server_user_list_single_value(
+            self, mock_pvh, mock_msr, mock_save_data):
+        # have to retry with simple get
+        mock_pvh.side_effect = iter([(None, None), ('1.1', '1.26')])
+        mock_conn = mock.MagicMock()
+        self.test_object.api_version_select_state = 'user'
+        # NOTE(TheJulia): Lets test this value explicitly because the
+        # minor number is actually the same.
+        self.test_object.os_ironic_api_version = ['1.01']
+        result = self.test_object.negotiate_version(mock_conn, None)
+        self.assertEqual('1.1', result)
+        self.assertEqual('negotiated',
+                         self.test_object.api_version_select_state)
+        self.assertEqual('1.1',
+                         self.test_object.os_ironic_api_version)
+        self.assertTrue(mock_msr.called)
+        self.assertEqual(2, mock_pvh.call_count)
+        self.assertEqual(1, mock_save_data.call_count)
+
+    @mock.patch.object(filecache, 'save_data', autospec=True)
+    @mock.patch.object(http.VersionNegotiationMixin, '_make_simple_request',
+                       autospec=True)
+    @mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers',
+                       autospec=True)
+    def test_negotiate_version_server_user_list_fails_latest(
+            self, mock_pvh, mock_msr, mock_save_data):
+        # have to retry with simple get
+        mock_pvh.side_effect = iter([(None, None), ('1.1', '1.2')])
+        mock_conn = mock.MagicMock()
+        self.test_object.api_version_select_state = 'user'
+        self.test_object.os_ironic_api_version = ['1.01', 'latest']
+        self.assertRaises(
+            ValueError,
+            self.test_object.negotiate_version,
+            mock_conn, self.response)
+        self.assertEqual('user',
+                         self.test_object.api_version_select_state)
+        self.assertEqual(['1.01', 'latest'],
+                         self.test_object.os_ironic_api_version)
+        self.assertEqual(2, mock_pvh.call_count)
+        self.assertEqual(0, mock_save_data.call_count)
+
     def test_get_server(self):
         host = 'ironic-host'
         port = '6385'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/ironicclient/tests/unit/osc/v1/fakes.py 
new/python-ironicclient-2.2.0/ironicclient/tests/unit/osc/v1/fakes.py
--- old/python-ironicclient-2.1.0/ironicclient/tests/unit/osc/v1/fakes.py       
2018-01-08 14:49:17.000000000 +0100
+++ new/python-ironicclient-2.2.0/ironicclient/tests/unit/osc/v1/fakes.py       
2018-01-26 01:40:27.000000000 +0100
@@ -137,6 +137,7 @@
              }
 
 VIFS = {'vifs': [{'id': 'aaa-aa'}]}
+TRAITS = ['CUSTOM_FOO', 'CUSTOM_BAR']
 
 baremetal_volume_connector_uuid = 'vvv-cccccc-vvvv'
 baremetal_volume_connector_type = 'iqn'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/ironicclient/tests/unit/osc/v1/test_baremetal_node.py
 
new/python-ironicclient-2.2.0/ironicclient/tests/unit/osc/v1/test_baremetal_node.py
--- 
old/python-ironicclient-2.1.0/ironicclient/tests/unit/osc/v1/test_baremetal_node.py
 2018-01-08 14:49:17.000000000 +0100
+++ 
new/python-ironicclient-2.2.0/ironicclient/tests/unit/osc/v1/test_baremetal_node.py
 2018-01-26 01:40:27.000000000 +0100
@@ -591,7 +591,7 @@
                    'Current RAID configuration', 'Reservation',
                    'Resource Class',
                    'Target Power State', 'Target Provision State',
-                   'Target RAID configuration',
+                   'Target RAID configuration', 'Traits',
                    'Updated At', 'Inspection Finished At',
                    'Inspection Started At', 'UUID', 'Name',
                    'Boot Interface', 'Console Interface',
@@ -627,6 +627,7 @@
             '',
             '',
             '',
+            '',
             baremetal_fakes.baremetal_uuid,
             baremetal_fakes.baremetal_name,
             '',
@@ -2663,3 +2664,186 @@
 
         self.baremetal_mock.node.inject_nmi.assert_called_once_with(
             'node_uuid')
+
+
+class TestListTraits(TestBaremetal):
+    def setUp(self):
+        super(TestListTraits, self).setUp()
+
+        self.baremetal_mock.node.get_traits.return_value = (
+            baremetal_fakes.TRAITS)
+
+        # Get the command object to test
+        self.cmd = baremetal_node.ListTraitsBaremetalNode(self.app, None)
+
+    def test_baremetal_list_traits(self):
+        arglist = ['node_uuid']
+        verifylist = [('node', 'node_uuid')]
+
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        self.cmd.take_action(parsed_args)
+
+        self.baremetal_mock.node.get_traits.assert_called_once_with(
+            'node_uuid')
+
+
+class TestAddTrait(TestBaremetal):
+    def setUp(self):
+        super(TestAddTrait, self).setUp()
+
+        # Get the command object to test
+        self.cmd = baremetal_node.AddTraitBaremetalNode(self.app, None)
+
+    def test_baremetal_add_trait(self):
+        arglist = ['node_uuid', 'CUSTOM_FOO']
+        verifylist = [('node', 'node_uuid'), ('traits', ['CUSTOM_FOO'])]
+
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        self.cmd.take_action(parsed_args)
+
+        self.baremetal_mock.node.add_trait.assert_called_once_with(
+            'node_uuid', 'CUSTOM_FOO')
+
+    def test_baremetal_add_traits_multiple(self):
+        arglist = ['node_uuid', 'CUSTOM_FOO', 'CUSTOM_BAR']
+        verifylist = [('node', 'node_uuid'),
+                      ('traits', ['CUSTOM_FOO', 'CUSTOM_BAR'])]
+
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        self.cmd.take_action(parsed_args)
+
+        expected_calls = [
+            mock.call('node_uuid', 'CUSTOM_FOO'),
+            mock.call('node_uuid', 'CUSTOM_BAR'),
+        ]
+        self.assertEqual(expected_calls,
+                         self.baremetal_mock.node.add_trait.call_args_list)
+
+    def test_baremetal_add_traits_multiple_with_failure(self):
+        arglist = ['node_uuid', 'CUSTOM_FOO', 'CUSTOM_BAR']
+        verifylist = [('node', 'node_uuid'),
+                      ('traits', ['CUSTOM_FOO', 'CUSTOM_BAR'])]
+
+        self.baremetal_mock.node.add_trait.side_effect = [
+            '', exc.ClientException]
+
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        self.assertRaises(exc.ClientException,
+                          self.cmd.take_action,
+                          parsed_args)
+
+        expected_calls = [
+            mock.call('node_uuid', 'CUSTOM_FOO'),
+            mock.call('node_uuid', 'CUSTOM_BAR'),
+        ]
+        self.assertEqual(expected_calls,
+                         self.baremetal_mock.node.add_trait.call_args_list)
+
+    def test_baremetal_add_traits_no_traits(self):
+        arglist = ['node_uuid']
+        verifylist = [('node', 'node_uuid')]
+
+        self.assertRaises(oscutils.ParserException,
+                          self.check_parser,
+                          self.cmd,
+                          arglist,
+                          verifylist)
+
+
+class TestRemoveTrait(TestBaremetal):
+    def setUp(self):
+        super(TestRemoveTrait, self).setUp()
+
+        # Get the command object to test
+        self.cmd = baremetal_node.RemoveTraitBaremetalNode(self.app, None)
+
+    def test_baremetal_remove_trait(self):
+        arglist = ['node_uuid', 'CUSTOM_FOO']
+        verifylist = [('node', 'node_uuid'), ('traits', ['CUSTOM_FOO'])]
+
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        self.cmd.take_action(parsed_args)
+
+        self.baremetal_mock.node.remove_trait.assert_called_once_with(
+            'node_uuid', 'CUSTOM_FOO')
+
+    def test_baremetal_remove_trait_multiple(self):
+        arglist = ['node_uuid', 'CUSTOM_FOO', 'CUSTOM_BAR']
+        verifylist = [('node', 'node_uuid'),
+                      ('traits', ['CUSTOM_FOO', 'CUSTOM_BAR'])]
+
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        self.cmd.take_action(parsed_args)
+
+        expected_calls = [
+            mock.call('node_uuid', 'CUSTOM_FOO'),
+            mock.call('node_uuid', 'CUSTOM_BAR'),
+        ]
+        self.assertEqual(expected_calls,
+                         self.baremetal_mock.node.remove_trait.call_args_list)
+
+    def test_baremetal_remove_trait_multiple_with_failure(self):
+        arglist = ['node_uuid', 'CUSTOM_FOO', 'CUSTOM_BAR']
+        verifylist = [('node', 'node_uuid'),
+                      ('traits', ['CUSTOM_FOO', 'CUSTOM_BAR'])]
+
+        self.baremetal_mock.node.remove_trait.side_effect = [
+            '', exc.ClientException]
+
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        self.assertRaises(exc.ClientException,
+                          self.cmd.take_action,
+                          parsed_args)
+
+        expected_calls = [
+            mock.call('node_uuid', 'CUSTOM_FOO'),
+            mock.call('node_uuid', 'CUSTOM_BAR'),
+        ]
+        self.assertEqual(expected_calls,
+                         self.baremetal_mock.node.remove_trait.call_args_list)
+
+    def test_baremetal_remove_trait_all(self):
+        arglist = ['node_uuid', '--all']
+        verifylist = [('node', 'node_uuid'), ('remove_all', True)]
+
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        self.cmd.take_action(parsed_args)
+
+        self.baremetal_mock.node.remove_all_traits.assert_called_once_with(
+            'node_uuid')
+
+    def test_baremetal_remove_trait_traits_and_all(self):
+        arglist = ['node_uuid', 'CUSTOM_FOO', '--all']
+        verifylist = [('node', 'node_uuid'),
+                      ('traits', ['CUSTOM_FOO']),
+                      ('remove_all', True)]
+
+        self.assertRaises(oscutils.ParserException,
+                          self.check_parser,
+                          self.cmd,
+                          arglist,
+                          verifylist)
+
+        self.baremetal_mock.node.remove_all_traits.assert_not_called()
+        self.baremetal_mock.node.remove_trait.assert_not_called()
+
+    def test_baremetal_remove_traits_no_traits_no_all(self):
+        arglist = ['node_uuid']
+        verifylist = [('node', 'node_uuid')]
+
+        self.assertRaises(oscutils.ParserException,
+                          self.check_parser,
+                          self.cmd,
+                          arglist,
+                          verifylist)
+
+        self.baremetal_mock.node.remove_all_traits.assert_not_called()
+        self.baremetal_mock.node.remove_trait.assert_not_called()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/ironicclient/tests/unit/osc/v1/test_baremetal_portgroup.py
 
new/python-ironicclient-2.2.0/ironicclient/tests/unit/osc/v1/test_baremetal_portgroup.py
--- 
old/python-ironicclient-2.1.0/ironicclient/tests/unit/osc/v1/test_baremetal_portgroup.py
    2018-01-08 14:49:17.000000000 +0100
+++ 
new/python-ironicclient-2.2.0/ironicclient/tests/unit/osc/v1/test_baremetal_portgroup.py
    2018-01-26 01:40:27.000000000 +0100
@@ -546,6 +546,23 @@
             [{'path': '/mode', 'value': new_portgroup_mode,
               'op': 'add'}])
 
+    def test_baremetal_portgroup_set_mode_int(self):
+        new_portgroup_mode = '4'
+        arglist = [
+            baremetal_fakes.baremetal_portgroup_uuid,
+            '--mode', new_portgroup_mode]
+        verifylist = [
+            ('portgroup', baremetal_fakes.baremetal_portgroup_uuid),
+            ('mode', new_portgroup_mode)]
+
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        self.cmd.take_action(parsed_args)
+        self.baremetal_mock.portgroup.update.assert_called_once_with(
+            baremetal_fakes.baremetal_portgroup_uuid,
+            [{'path': '/mode', 'value': new_portgroup_mode,
+              'op': 'add'}])
+
     def test_baremetal_portgroup_set_node_uuid(self):
         new_node_uuid = 'nnnnnn-uuuuuuuu'
         arglist = [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/ironicclient/tests/unit/test_client.py 
new/python-ironicclient-2.2.0/ironicclient/tests/unit/test_client.py
--- old/python-ironicclient-2.1.0/ironicclient/tests/unit/test_client.py        
2018-01-08 14:49:17.000000000 +0100
+++ new/python-ironicclient-2.2.0/ironicclient/tests/unit/test_client.py        
2018-01-26 01:40:03.000000000 +0100
@@ -58,7 +58,13 @@
             interface=kwargs.get('os_endpoint_type') or 'publicURL',
             region_name=kwargs.get('os_region_name'))
         if 'os_ironic_api_version' in kwargs:
+            # NOTE(TheJulia): This does not test the negotiation logic
+            # as a request must be triggered in order for any verison
+            # negotiation actions to occur.
             self.assertEqual(0, mock_retrieve_data.call_count)
+            self.assertEqual(kwargs['os_ironic_api_version'],
+                             client.current_api_version)
+            self.assertFalse(client.is_api_version_negotiated)
         else:
             mock_retrieve_data.assert_called_once_with(
                 host='localhost',
@@ -132,6 +138,17 @@
         }
         self._test_get_client(**kwargs)
 
+    def test_get_client_with_api_version_list(self):
+        kwargs = {
+            'os_project_name': 'PROJECT_NAME',
+            'os_username': 'USERNAME',
+            'os_password': 'PASSWORD',
+            'os_auth_url': 'http://localhost:35357/v2.0',
+            'os_auth_token': '',
+            'os_ironic_api_version': ['1.1', '1.99'],
+        }
+        self._test_get_client(**kwargs)
+
     def test_get_client_with_api_version_numeric(self):
         kwargs = {
             'os_project_name': 'PROJECT_NAME',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/ironicclient/tests/unit/v1/test_client.py 
new/python-ironicclient-2.2.0/ironicclient/tests/unit/v1/test_client.py
--- old/python-ironicclient-2.1.0/ironicclient/tests/unit/v1/test_client.py     
2018-01-08 14:49:17.000000000 +0100
+++ new/python-ironicclient-2.2.0/ironicclient/tests/unit/v1/test_client.py     
2018-01-26 01:40:03.000000000 +0100
@@ -49,6 +49,16 @@
             os_ironic_api_version=os_ironic_api_version,
             api_version_select_state='default')
 
+    def test_client_user_api_version_latest_with_downgrade(self,
+                                                           http_client_mock):
+        endpoint = 'http://ironic:6385'
+        token = 'safe_token'
+        os_ironic_api_version = 'latest'
+
+        self.assertRaises(ValueError, client.Client, endpoint,
+                          token=token, allow_api_version_downgrade=True,
+                          os_ironic_api_version=os_ironic_api_version)
+
     @mock.patch.object(filecache, 'retrieve_data', autospec=True)
     def test_client_cache_api_version(self, cache_mock, http_client_mock):
         endpoint = 'http://ironic:6385'
@@ -93,3 +103,19 @@
         self.assertIsInstance(cl.port, client.port.PortManager)
         self.assertIsInstance(cl.driver, client.driver.DriverManager)
         self.assertIsInstance(cl.chassis, client.chassis.ChassisManager)
+
+    def test_negotiate_api_version(self, http_client_mock):
+        endpoint = 'http://ironic:6385'
+        token = 'safe_token'
+        os_ironic_api_version = 'latest'
+        cl = client.Client(endpoint, token=token,
+                           os_ironic_api_version=os_ironic_api_version)
+
+        cl.negotiate_api_version()
+        http_client_mock.assert_called_once_with(
+            endpoint, api_version_select_state='user',
+            os_ironic_api_version='latest', token=token)
+        # TODO(TheJulia): We should verify that negotiate_version
+        # is being called in the client and returns a version,
+        # although mocking might need to be restrutured to
+        # properly achieve that.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/ironicclient/tests/unit/v1/test_node.py 
new/python-ironicclient-2.2.0/ironicclient/tests/unit/v1/test_node.py
--- old/python-ironicclient-2.1.0/ironicclient/tests/unit/v1/test_node.py       
2018-01-08 14:49:17.000000000 +0100
+++ new/python-ironicclient-2.2.0/ironicclient/tests/unit/v1/test_node.py       
2018-01-26 01:40:27.000000000 +0100
@@ -103,6 +103,7 @@
                                              "async": "true"}}
 
 VIFS = {'vifs': [{'id': 'aaa-aaa'}]}
+TRAITS = {'traits': ['CUSTOM_FOO', 'CUSTOM_BAR']}
 
 CREATE_NODE = copy.deepcopy(NODE1)
 del CREATE_NODE['uuid']
@@ -448,6 +449,32 @@
             {},
             VIFS,
         ),
+    },
+    '/v1/nodes/%s/traits' % NODE1['uuid']:
+    {
+        'GET': (
+            {},
+            TRAITS,
+        ),
+        'PUT': (
+            {},
+            None,
+        ),
+        'DELETE': (
+            {},
+            None,
+        ),
+    },
+    '/v1/nodes/%s/traits/CUSTOM_FOO' % NODE1['uuid']:
+    {
+        'PUT': (
+            {},
+            None,
+        ),
+        'DELETE': (
+            {},
+            None,
+        ),
     }
 }
 
@@ -1641,3 +1668,49 @@
         self.assertEqual(4, mock_get.call_count)
         mock_sleep.assert_called_with(node._DEFAULT_POLL_INTERVAL)
         self.assertEqual(3, mock_sleep.call_count)
+
+    def test_node_get_traits(self):
+        traits = self.mgr.get_traits(NODE1['uuid'])
+        expect = [
+            ('GET', '/v1/nodes/%s/traits' % NODE1['uuid'], {}, None),
+        ]
+        self.assertEqual(expect, self.api.calls)
+        self.assertEqual(TRAITS['traits'], traits)
+
+    def test_node_add_trait(self):
+        trait = 'CUSTOM_FOO'
+        resp = self.mgr.add_trait(NODE1['uuid'], trait)
+        expect = [
+            ('PUT', '/v1/nodes/%s/traits/%s' % (NODE1['uuid'], trait),
+                {}, None),
+        ]
+        self.assertEqual(expect, self.api.calls)
+        self.assertIsNone(resp)
+
+    def test_node_set_traits(self):
+        traits = ['CUSTOM_FOO', 'CUSTOM_BAR']
+        resp = self.mgr.set_traits(NODE1['uuid'], traits)
+        expect = [
+            ('PUT', '/v1/nodes/%s/traits' % NODE1['uuid'],
+                {}, {'traits': traits}),
+        ]
+        self.assertEqual(expect, self.api.calls)
+        self.assertIsNone(resp)
+
+    def test_node_remove_all_traits(self):
+        resp = self.mgr.remove_all_traits(NODE1['uuid'])
+        expect = [
+            ('DELETE', '/v1/nodes/%s/traits' % NODE1['uuid'], {}, None),
+        ]
+        self.assertEqual(expect, self.api.calls)
+        self.assertIsNone(resp)
+
+    def test_node_remove_trait(self):
+        trait = 'CUSTOM_FOO'
+        resp = self.mgr.remove_trait(NODE1['uuid'], trait)
+        expect = [
+            ('DELETE', '/v1/nodes/%s/traits/%s' % (NODE1['uuid'], trait),
+                {}, None),
+        ]
+        self.assertEqual(expect, self.api.calls)
+        self.assertIsNone(resp)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/ironicclient/tests/unit/v1/test_node_shell.py 
new/python-ironicclient-2.2.0/ironicclient/tests/unit/v1/test_node_shell.py
--- old/python-ironicclient-2.1.0/ironicclient/tests/unit/v1/test_node_shell.py 
2018-01-08 14:49:17.000000000 +0100
+++ new/python-ironicclient-2.2.0/ironicclient/tests/unit/v1/test_node_shell.py 
2018-01-26 01:40:27.000000000 +0100
@@ -65,6 +65,7 @@
                'resource_class',
                'target_power_state',
                'target_provision_state',
+               'traits',
                'updated_at',
                'inspection_finished_at',
                'inspection_started_at',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-ironicclient-2.1.0/ironicclient/v1/client.py 
new/python-ironicclient-2.2.0/ironicclient/v1/client.py
--- old/python-ironicclient-2.1.0/ironicclient/v1/client.py     2018-01-08 
14:49:17.000000000 +0100
+++ new/python-ironicclient-2.2.0/ironicclient/v1/client.py     2018-01-26 
01:40:03.000000000 +0100
@@ -41,7 +41,17 @@
         """Initialize a new client for the Ironic v1 API."""
         allow_downgrade = kwargs.pop('allow_api_version_downgrade', False)
         if kwargs.get('os_ironic_api_version'):
+            # TODO(TheJulia): We should sanity check os_ironic_api_version
+            # against our maximum suported version, so the client fails
+            # immediately upon an unsupported version being provided.
+            # This logic should also likely live in common/http.py
             if allow_downgrade:
+                if kwargs['os_ironic_api_version'] == 'latest':
+                    raise ValueError(
+                        "Invalid configuration defined. "
+                        "The os_ironic_api_versioncan not be set "
+                        "to 'latest' while allow_api_version_downgrade "
+                        "is set.")
                 # NOTE(dtantsur): here we allow the HTTP client to negotiate a
                 # lower version if the requested is too high
                 kwargs['api_version_select_state'] = "default"
@@ -76,3 +86,26 @@
             self.http_client)
         self.driver = driver.DriverManager(self.http_client)
         self.portgroup = portgroup.PortgroupManager(self.http_client)
+
+    @property
+    def current_api_version(self):
+        """Return the current API version in use.
+
+        This returns the version of the REST API that the API client
+        is presently set to request. This value may change as a result
+        of API version negotiation.
+        """
+        return self.http_client.os_ironic_api_version
+
+    @property
+    def is_api_version_negotiated(self):
+        """Returns True if microversion negotiation has occured."""
+        return self.http_client.api_version_select_state == 'negotiated'
+
+    def negotiate_api_version(self):
+        """Triggers negotiation with the remote API endpoint.
+
+        :returns: the negotiated API version.
+        """
+        return self.http_client.negotiate_version(
+            self.http_client.session, None)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-ironicclient-2.1.0/ironicclient/v1/node.py 
new/python-ironicclient-2.2.0/ironicclient/v1/node.py
--- old/python-ironicclient-2.1.0/ironicclient/v1/node.py       2018-01-08 
14:49:17.000000000 +0100
+++ new/python-ironicclient-2.2.0/ironicclient/v1/node.py       2018-01-26 
01:40:27.000000000 +0100
@@ -553,6 +553,53 @@
         path = "%s/vendor_passthru/methods" % node_ident
         return self._get_as_dict(path)
 
+    def get_traits(self, node_ident):
+        """Get traits for a node.
+
+        :param node_ident: node UUID or name.
+        """
+        path = "%s/traits" % node_ident
+        return self._list_primitives(self._path(path), 'traits')
+
+    def add_trait(self, node_ident, trait):
+        """Add a trait to a node.
+
+        :param node_ident: node UUID or name.
+        :param trait: trait to add to the node.
+        """
+        path = "%s/traits/%s" % (node_ident, trait)
+        return self.update(path, None, http_method='PUT')
+
+    def set_traits(self, node_ident, traits):
+        """Set traits for a node.
+
+        Removes any existing traits and adds the traits passed in to this
+        method.
+
+        :param node_ident: node UUID or name.
+        :param traits: list of traits to add to the node.
+        """
+        path = "%s/traits" % node_ident
+        body = {'traits': traits}
+        return self.update(path, body, http_method='PUT')
+
+    def remove_trait(self, node_ident, trait):
+        """Remove a trait from a node.
+
+        :param node_ident: node UUID or name.
+        :param trait: trait to remove from the node.
+        """
+        path = "%s/traits/%s" % (node_ident, trait)
+        return self.delete(path)
+
+    def remove_all_traits(self, node_ident):
+        """Remove all traits from a node.
+
+        :param node_ident: node UUID or name.
+        """
+        path = "%s/traits" % node_ident
+        return self.delete(path)
+
     def wait_for_provision_state(self, node_ident, expected_state,
                                  timeout=0,
                                  poll_interval=_DEFAULT_POLL_INTERVAL,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/ironicclient/v1/resource_fields.py 
new/python-ironicclient-2.2.0/ironicclient/v1/resource_fields.py
--- old/python-ironicclient-2.1.0/ironicclient/v1/resource_fields.py    
2018-01-08 14:49:17.000000000 +0100
+++ new/python-ironicclient-2.2.0/ironicclient/v1/resource_fields.py    
2018-01-26 01:40:27.000000000 +0100
@@ -87,6 +87,7 @@
         'target_power_state': 'Target Power State',
         'target_provision_state': 'Target Provision State',
         'target_raid_config': 'Target RAID configuration',
+        'traits': 'Traits',
         'type': 'Type',
         'updated_at': 'Updated At',
         'uuid': 'UUID',
@@ -210,6 +211,7 @@
      'target_power_state',
      'target_provision_state',
      'target_raid_config',
+     'traits',
      'updated_at',
      'inspection_finished_at',
      'inspection_started_at',
@@ -239,6 +241,7 @@
         'properties',
         'raid_config',
         'target_raid_config',
+        'traits',
     ])
 NODE_RESOURCE = Resource(
     ['uuid',
@@ -319,6 +322,10 @@
     ['id'],
 )
 
+TRAIT_RESOURCE = Resource(
+    ['traits'],
+)
+
 # Drivers
 DRIVER_DETAILED_RESOURCE = Resource(
     ['name',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/python_ironicclient.egg-info/PKG-INFO 
new/python-ironicclient-2.2.0/python_ironicclient.egg-info/PKG-INFO
--- old/python-ironicclient-2.1.0/python_ironicclient.egg-info/PKG-INFO 
2018-01-08 14:52:17.000000000 +0100
+++ new/python-ironicclient-2.2.0/python_ironicclient.egg-info/PKG-INFO 
2018-01-26 01:44:32.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: python-ironicclient
-Version: 2.1.0
+Version: 2.2.0
 Summary: OpenStack Bare Metal Provisioning API Client Library
 Home-page: https://docs.openstack.org/python-ironicclient/latest/
 Author: OpenStack
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/python_ironicclient.egg-info/SOURCES.txt 
new/python-ironicclient-2.2.0/python_ironicclient.egg-info/SOURCES.txt
--- old/python-ironicclient-2.1.0/python_ironicclient.egg-info/SOURCES.txt      
2018-01-08 14:52:18.000000000 +0100
+++ new/python-ironicclient-2.2.0/python_ironicclient.egg-info/SOURCES.txt      
2018-01-26 01:44:33.000000000 +0100
@@ -173,11 +173,14 @@
 releasenotes/notes/add-volume-target-api-e062303f4b3b40ef.yaml
 releasenotes/notes/add-volume-target-cli-e062303f4b3b40f0.yaml
 releasenotes/notes/add_api_versions-a59e5b6899833c33.yaml
+releasenotes/notes/allow-api-user-to-use-latest-6b80e9f584eaaa4e.yaml
+releasenotes/notes/allow-client-to-request-list-of-versions-88f019cad76e6464.yaml
 releasenotes/notes/bug-1524745-adds-node-create-args-a7ace744515e5943.yaml
 
releasenotes/notes/bug-1524745-extend-driver-list-and-driver-show-800d96393aa17342.yaml
 releasenotes/notes/bug-1524745-update-baremetal-node-set-c1ac57de0d481efe.yaml
 
releasenotes/notes/bug-1712935-allow-os_baremetal_api_version_env_var_to_be_latest-28c8eed24f389673.yaml
 releasenotes/notes/bug-1724974-add-wanboot-to-supported-boot-devices.yaml
+releasenotes/notes/bug-1745099-allow-integer-portgroup-mode-6be4d3b35e216486.yaml
 releasenotes/notes/continue-del-next-node-8827e67e1c41a0a5.yaml
 releasenotes/notes/deprecate-ironic-cli-686b7a238ddf3e25.yaml
 
releasenotes/notes/display-empty-string-for-chassis-uuid-if-it-is-empty-a5471c3aa740a27d.yaml
@@ -242,6 +245,7 @@
 releasenotes/notes/soft-reboot-poweroff-e33d078a05db3894.yaml
 releasenotes/notes/start-using-reno-ccd220efa2c7022a.yaml
 releasenotes/notes/switch-requests-8304d4465a8976b1.yaml
+releasenotes/notes/traits-support-8864f6816abecdb2.yaml
 releasenotes/source/conf.py
 releasenotes/source/index.rst
 releasenotes/source/mitaka.rst
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/python_ironicclient.egg-info/entry_points.txt 
new/python-ironicclient-2.2.0/python_ironicclient.egg-info/entry_points.txt
--- old/python-ironicclient-2.1.0/python_ironicclient.egg-info/entry_points.txt 
2018-01-08 14:52:17.000000000 +0100
+++ new/python-ironicclient-2.2.0/python_ironicclient.egg-info/entry_points.txt 
2018-01-26 01:44:32.000000000 +0100
@@ -16,6 +16,7 @@
 baremetal_driver_raid_property_list = 
ironicclient.osc.v1.baremetal_driver:ListBaremetalDriverRaidProperty
 baremetal_driver_show = 
ironicclient.osc.v1.baremetal_driver:ShowBaremetalDriver
 baremetal_node_abort = ironicclient.osc.v1.baremetal_node:AbortBaremetalNode
+baremetal_node_add_trait = 
ironicclient.osc.v1.baremetal_node:AddTraitBaremetalNode
 baremetal_node_adopt = ironicclient.osc.v1.baremetal_node:AdoptBaremetalNode
 baremetal_node_boot_device_set = 
ironicclient.osc.v1.baremetal_node:BootdeviceSetBaremetalNode
 baremetal_node_boot_device_show = 
ironicclient.osc.v1.baremetal_node:BootdeviceShowBaremetalNode
@@ -39,8 +40,10 @@
 baremetal_node_provide = 
ironicclient.osc.v1.baremetal_node:ProvideBaremetalNode
 baremetal_node_reboot = ironicclient.osc.v1.baremetal_node:RebootBaremetalNode
 baremetal_node_rebuild = 
ironicclient.osc.v1.baremetal_node:RebuildBaremetalNode
+baremetal_node_remove_trait = 
ironicclient.osc.v1.baremetal_node:RemoveTraitBaremetalNode
 baremetal_node_set = ironicclient.osc.v1.baremetal_node:SetBaremetalNode
 baremetal_node_show = ironicclient.osc.v1.baremetal_node:ShowBaremetalNode
+baremetal_node_trait_list = 
ironicclient.osc.v1.baremetal_node:ListTraitsBaremetalNode
 baremetal_node_undeploy = 
ironicclient.osc.v1.baremetal_node:UndeployBaremetalNode
 baremetal_node_unset = ironicclient.osc.v1.baremetal_node:UnsetBaremetalNode
 baremetal_node_validate = 
ironicclient.osc.v1.baremetal_node:ValidateBaremetalNode
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/python_ironicclient.egg-info/pbr.json 
new/python-ironicclient-2.2.0/python_ironicclient.egg-info/pbr.json
--- old/python-ironicclient-2.1.0/python_ironicclient.egg-info/pbr.json 
2018-01-08 14:52:17.000000000 +0100
+++ new/python-ironicclient-2.2.0/python_ironicclient.egg-info/pbr.json 
2018-01-26 01:44:32.000000000 +0100
@@ -1 +1 @@
-{"git_version": "5a427ee", "is_release": true}
\ No newline at end of file
+{"git_version": "683b7c6", "is_release": true}
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/releasenotes/notes/allow-api-user-to-use-latest-6b80e9f584eaaa4e.yaml
 
new/python-ironicclient-2.2.0/releasenotes/notes/allow-api-user-to-use-latest-6b80e9f584eaaa4e.yaml
--- 
old/python-ironicclient-2.1.0/releasenotes/notes/allow-api-user-to-use-latest-6b80e9f584eaaa4e.yaml
 1970-01-01 01:00:00.000000000 +0100
+++ 
new/python-ironicclient-2.2.0/releasenotes/notes/allow-api-user-to-use-latest-6b80e9f584eaaa4e.yaml
 2018-01-26 01:40:03.000000000 +0100
@@ -0,0 +1,26 @@
+---
+features:
+  - |
+    Allows a python API user to pass ``latest`` to the client creation request
+    for the ``os_ironic_api_version`` parameter. The version utilized for REST
+    API requests will, as a result, be the highest available version
+    understood by both the ironicclient library and the server.
+  - |
+    Adds base client properties to provide insight to a python API user of
+    what the current REST API version that will be utilized, and if API
+    version negotiation has occured.
+    These new properties are ``client.current_api_version`` and
+    ``client.is_api_version_negotiated`` respectively.
+  - |
+    Adds additional base client method to allow a python API user to trigger
+    version negotiation and return the negotiated version. This new method is
+    ``client.negotiate_api_version()``.
+other:
+  - |
+    The maximum supported version supported for negotiation is now defined
+    in the ``common/http.py`` file. Any new feature added to the API client
+    library must increment this version.
+  - |
+    The maximum known version supported by the ``OpenStackClient`` plugin is
+    now defined by the maximum supported version for API negotiation as
+    defined in the ``common/http.py`` file.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/releasenotes/notes/allow-client-to-request-list-of-versions-88f019cad76e6464.yaml
 
new/python-ironicclient-2.2.0/releasenotes/notes/allow-client-to-request-list-of-versions-88f019cad76e6464.yaml
--- 
old/python-ironicclient-2.1.0/releasenotes/notes/allow-client-to-request-list-of-versions-88f019cad76e6464.yaml
     1970-01-01 01:00:00.000000000 +0100
+++ 
new/python-ironicclient-2.2.0/releasenotes/notes/allow-client-to-request-list-of-versions-88f019cad76e6464.yaml
     2018-01-26 01:40:03.000000000 +0100
@@ -0,0 +1,7 @@
+---
+features:
+  - |
+    The ``os_ironic_api_version`` parameter now accepts a list of REST
+    API micro-versions to attempt to negotiate with the remote server.
+    The highest available microversion in the list will be negotiated
+    for the remaining lifetime of the client session.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/releasenotes/notes/bug-1745099-allow-integer-portgroup-mode-6be4d3b35e216486.yaml
 
new/python-ironicclient-2.2.0/releasenotes/notes/bug-1745099-allow-integer-portgroup-mode-6be4d3b35e216486.yaml
--- 
old/python-ironicclient-2.1.0/releasenotes/notes/bug-1745099-allow-integer-portgroup-mode-6be4d3b35e216486.yaml
     1970-01-01 01:00:00.000000000 +0100
+++ 
new/python-ironicclient-2.2.0/releasenotes/notes/bug-1745099-allow-integer-portgroup-mode-6be4d3b35e216486.yaml
     2018-01-26 01:40:27.000000000 +0100
@@ -0,0 +1,7 @@
+---
+fixes:
+  - |
+    Fixes `bug 1745099
+    <https://bugs.launchpad.net/python-ironicclient/+bug/1745099>`_,
+    which prevented a port group's mode from being set to an integer value
+    via the ``openstack baremetal port group set`` command.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-ironicclient-2.1.0/releasenotes/notes/traits-support-8864f6816abecdb2.yaml
 
new/python-ironicclient-2.2.0/releasenotes/notes/traits-support-8864f6816abecdb2.yaml
--- 
old/python-ironicclient-2.1.0/releasenotes/notes/traits-support-8864f6816abecdb2.yaml
       1970-01-01 01:00:00.000000000 +0100
+++ 
new/python-ironicclient-2.2.0/releasenotes/notes/traits-support-8864f6816abecdb2.yaml
       2018-01-26 01:40:27.000000000 +0100
@@ -0,0 +1,20 @@
+---
+features:
+  - |
+    Adds support for reading and modifying traits for a node, including adding
+    traits to the detailed output of a node. This is available starting
+    with Bare Metal API version 1.37.
+
+    The new commands are:
+
+    * ``openstack baremetal node trait list <node>``
+    * ``openstack baremetal node add trait <node> <trait> [...]``
+    * ``openstack baremetal node remove trait <node> [<trait> [...]] [--all]``
+
+    It also adds the following methods to the Python SDK:
+
+    * ``NodeManager.get_traits``
+    * ``NodeManager.add_trait``
+    * ``NodeManager.set_traits``
+    * ``NodeManager.remove_trait``
+    * ``NodeManager.remove_all_traits``
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-ironicclient-2.1.0/setup.cfg 
new/python-ironicclient-2.2.0/setup.cfg
--- old/python-ironicclient-2.1.0/setup.cfg     2018-01-08 14:52:18.000000000 
+0100
+++ new/python-ironicclient-2.2.0/setup.cfg     2018-01-26 01:44:33.000000000 
+0100
@@ -40,6 +40,7 @@
        baremetal_driver_raid_property_list = 
ironicclient.osc.v1.baremetal_driver:ListBaremetalDriverRaidProperty
        baremetal_driver_show = 
ironicclient.osc.v1.baremetal_driver:ShowBaremetalDriver
        baremetal_node_abort = 
ironicclient.osc.v1.baremetal_node:AbortBaremetalNode
+       baremetal_node_add_trait = 
ironicclient.osc.v1.baremetal_node:AddTraitBaremetalNode
        baremetal_node_adopt = 
ironicclient.osc.v1.baremetal_node:AdoptBaremetalNode
        baremetal_node_boot_device_set = 
ironicclient.osc.v1.baremetal_node:BootdeviceSetBaremetalNode
        baremetal_node_boot_device_show = 
ironicclient.osc.v1.baremetal_node:BootdeviceShowBaremetalNode
@@ -62,8 +63,10 @@
        baremetal_node_provide = 
ironicclient.osc.v1.baremetal_node:ProvideBaremetalNode
        baremetal_node_reboot = 
ironicclient.osc.v1.baremetal_node:RebootBaremetalNode
        baremetal_node_rebuild = 
ironicclient.osc.v1.baremetal_node:RebuildBaremetalNode
+       baremetal_node_remove_trait = 
ironicclient.osc.v1.baremetal_node:RemoveTraitBaremetalNode
        baremetal_node_set = ironicclient.osc.v1.baremetal_node:SetBaremetalNode
        baremetal_node_show = 
ironicclient.osc.v1.baremetal_node:ShowBaremetalNode
+       baremetal_node_trait_list = 
ironicclient.osc.v1.baremetal_node:ListTraitsBaremetalNode
        baremetal_node_undeploy = 
ironicclient.osc.v1.baremetal_node:UndeployBaremetalNode
        baremetal_node_unset = 
ironicclient.osc.v1.baremetal_node:UnsetBaremetalNode
        baremetal_node_validate = 
ironicclient.osc.v1.baremetal_node:ValidateBaremetalNode
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-ironicclient-2.1.0/test-requirements.txt 
new/python-ironicclient-2.2.0/test-requirements.txt
--- old/python-ironicclient-2.1.0/test-requirements.txt 2018-01-08 
14:49:17.000000000 +0100
+++ new/python-ironicclient-2.2.0/test-requirements.txt 2018-01-26 
01:40:03.000000000 +0100
@@ -8,10 +8,10 @@
 requests-mock>=1.1.0 # Apache-2.0
 mock>=2.0.0 # BSD
 Babel!=2.4.0,>=2.3.4 # BSD
-openstackdocstheme>=1.17.0 # Apache-2.0
+openstackdocstheme>=1.18.1 # Apache-2.0
 reno>=2.5.0 # Apache-2.0
-oslotest>=1.10.0 # Apache-2.0
-sphinx>=1.6.2 # BSD
+oslotest>=3.2.0 # Apache-2.0
+sphinx!=1.6.6,>=1.6.2 # BSD
 testtools>=2.2.0 # MIT
 tempest>=17.1.0 # Apache-2.0
 os-testr>=1.0.0 # Apache-2.0


Reply via email to