Add openstackclient support

This adds support for the recommended CLI using the OpenStackClient,
without modifying the existing Blazar shell CLI.

The existing shell command classes are used, by introducing a check in
the base comand class to use either the client passed by Blazar shell,
or the client using the osc_lib client_manager.

The argument --physical-reservation is also removed for the create lease
command when using the OpenStack client.

Implements: blueprint openstackclient-support
Change-Id: I97a7b91f0d05efc887307ac167e5c368276d4f81
This commit is contained in:
Mark Powers 2021-06-09 11:06:38 -05:00
parent 1cf4d73dd5
commit f2277b5f7a
10 changed files with 265 additions and 104 deletions

View File

@ -81,7 +81,12 @@ class BlazarCommand(OpenStackCommand):
# self.formatters['table'] = TableFormatter()
def get_client(self):
return self.app.client
# client_manager.reservation is used for osc_lib, and should be used
# if it exists
if hasattr(self.app, 'client_manager'):
return self.app.client_manager.reservation
else:
return self.app.client
def get_parser(self, prog_name):
parser = super(BlazarCommand, self).get_parser(prog_name)

View File

View File

@ -0,0 +1,66 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from osc_lib import utils
LOG = logging.getLogger(__name__)
DEFAULT_API_VERSION = '1'
# Required by the OSC plugin interface
API_NAME = 'reservation'
API_VERSION_OPTION = 'os_reservations_api_version'
API_VERSIONS = {
'1': 'blazarclient.v1.client.Client',
}
# Required by the OSC plugin interface
def make_client(instance):
reservation_client = utils.get_client_class(
API_NAME,
instance._api_version[API_NAME],
API_VERSIONS)
LOG.debug("Instantiating reservation client: %s", reservation_client)
client = reservation_client(
instance._api_version[API_NAME],
session=instance.session,
endpoint_override=instance.get_endpoint_for_service_type(
API_NAME,
interface=instance.interface,
region_name=instance._region_name)
)
return client
# Required by the OSC plugin interface
def build_option_parser(parser):
"""Hook to add global options.
Called from openstackclient.shell.OpenStackShell.__init__()
after the builtin parser has been initialized. This is
where a plugin can add global options such as an API version setting.
:param argparse.ArgumentParser parser: The parser object that has been
initialized by OpenStackShell.
"""
parser.add_argument(
"--os-reservation-api-version",
metavar="<reservation-api-version>",
help="Reservation API version, default="
"{} (Env: OS_RESERVATION_API_VERSION)".format(
DEFAULT_API_VERSION)
)
return parser

View File

@ -60,9 +60,19 @@ class BlazarCommandTestCase(tests.TestCase):
self.command = command.BlazarCommand(self.app, [])
def test_get_client(self):
# Test that either client_manager.reservation or client is used,
# whichever exists
client_manager = self.app.client_manager
del self.app.client_manager
client = self.command.get_client()
self.assertEqual(self.app.client, client)
self.app.client_manager = client_manager
del self.app.client
client = self.command.get_client()
self.assertEqual(self.app.client_manager.reservation, client)
def test_get_parser(self):
self.command.get_parser('TestCase')
self.parser.assert_called_once_with('TestCase')

View File

@ -0,0 +1,36 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from unittest import mock
from blazarclient.osc import plugin
from blazarclient import tests
class ReservationPluginTests(tests.TestCase):
@mock.patch("blazarclient.v1.client.Client")
def test_make_client(self, mock_client):
instance = mock.Mock()
instance._api_version = {"reservation": "1"}
endpoint = "blazar_endpoint"
instance.get_endpoint_for_service_type = mock.Mock(
return_value=endpoint
)
plugin.make_client(instance)
mock_client.assert_called_with(
"1",
session=instance.session,
endpoint_override=endpoint
)

View File

@ -91,7 +91,7 @@ class ShowLease(command.ShowCommand):
log = logging.getLogger(__name__ + '.ShowLease')
class CreateLease(command.CreateCommand):
class CreateLeaseBase(command.CreateCommand):
"""Create a lease."""
resource = 'lease'
json_indent = 4
@ -100,7 +100,7 @@ class CreateLease(command.CreateCommand):
default_end = _utc_now() + datetime.timedelta(days=1)
def get_parser(self, prog_name):
parser = super(CreateLease, self).get_parser(prog_name)
parser = super(CreateLeaseBase, self).get_parser(prog_name)
parser.add_argument(
'name', metavar=self.resource.upper(),
help='Name for the %s' % self.resource
@ -126,22 +126,6 @@ class CreateLease(command.CreateCommand):
'the end of the lease (default: depends on system default)',
default=None
)
parser.add_argument(
'--physical-reservation',
metavar="<min=int,max=int,hypervisor_properties=str,"
"resource_properties=str,before_end=str>",
action='append',
dest='physical_reservations',
help='Create a reservation for physical compute hosts. '
'Specify option multiple times to create multiple '
'reservations. '
'min: minimum number of hosts to reserve. '
'max: maximum number of hosts to reserve. '
'hypervisor_properties: JSON string, see doc. '
'resource_properties: JSON string, see doc. '
'before_end: JSON string, see doc. ',
default=[]
)
parser.add_argument(
'--reservation',
metavar="<key=value>",
@ -165,36 +149,12 @@ class CreateLease(command.CreateCommand):
return parser
def args2body(self, parsed_args):
def parse_params(str_params, default):
request_params = {}
prog = re.compile('^(?:(.*),)?(%s)=(.*)$'
% "|".join(default.keys()))
while str_params != "":
match = prog.search(str_params)
if match is None:
raise exception.IncorrectLease(err_msg)
self.log.info("Matches: %s", match.groups())
k, v = match.group(2, 3)
if k in request_params.keys():
raise exception.DuplicatedLeaseParameters(err_msg)
else:
if strutils.is_int_like(v):
request_params[k] = int(v)
elif isinstance(defaults[k], list):
request_params[k] = jsonutils.loads(v)
else:
request_params[k] = v
str_params = match.group(1) if match.group(1) else ""
request_params.update({k: v for k, v in default.items()
if k not in request_params.keys() and
v is not None})
return request_params
params = self._generate_params(parsed_args)
if not params['reservations']:
raise exception.IncorrectLease
return params
def _generate_params(self, parsed_args):
params = {}
if parsed_args.name:
params['name'] = parsed_args.name
@ -243,6 +203,115 @@ class CreateLease(command.CreateCommand):
params['reservations'] = []
params['events'] = []
reservations = []
for res_str in parsed_args.reservations:
err_msg = ("Invalid reservation argument '%s'. "
"Reservation arguments must be of the "
"form --reservation <key=value>"
% res_str)
if "physical:host" in res_str:
defaults = CREATE_RESERVATION_KEYS['physical:host']
elif "virtual:instance" in res_str:
defaults = CREATE_RESERVATION_KEYS['virtual:instance']
elif "virtual:floatingip" in res_str:
defaults = CREATE_RESERVATION_KEYS['virtual:floatingip']
else:
defaults = CREATE_RESERVATION_KEYS['others']
res_info = self._parse_params(res_str, defaults, err_msg)
reservations.append(res_info)
if reservations:
params['reservations'] += reservations
events = []
for event_str in parsed_args.events:
err_msg = ("Invalid event argument '%s'. "
"Event arguments must be of the "
"form --event <event_type=str,event_date=time>"
% event_str)
event_info = {"event_type": "", "event_date": ""}
for kv_str in event_str.split(","):
try:
k, v = kv_str.split("=", 1)
except ValueError:
raise exception.IncorrectLease(err_msg)
if k in event_info:
event_info[k] = v
else:
raise exception.IncorrectLease(err_msg)
if not event_info['event_type'] and not event_info['event_date']:
raise exception.IncorrectLease(err_msg)
event_date = event_info['event_date']
try:
date = datetime.datetime.strptime(event_date, '%Y-%m-%d %H:%M')
event_date = datetime.datetime.strftime(date, '%Y-%m-%d %H:%M')
event_info['event_date'] = event_date
except ValueError:
raise exception.IncorrectLease
events.append(event_info)
if events:
params['events'] = events
return params
def _parse_params(self, str_params, default, err_msg):
request_params = {}
prog = re.compile('^(?:(.*),)?(%s)=(.*)$'
% "|".join(default.keys()))
while str_params != "":
match = prog.search(str_params)
if match is None:
raise exception.IncorrectLease(err_msg)
self.log.info("Matches: %s", match.groups())
k, v = match.group(2, 3)
if k in request_params.keys():
raise exception.DuplicatedLeaseParameters(err_msg)
else:
if strutils.is_int_like(v):
request_params[k] = int(v)
elif isinstance(default[k], list):
request_params[k] = jsonutils.loads(v)
else:
request_params[k] = v
str_params = match.group(1) if match.group(1) else ""
request_params.update({k: v for k, v in default.items()
if k not in request_params.keys() and
v is not None})
return request_params
class CreateLease(CreateLeaseBase):
def get_parser(self, prog_name):
parser = super(CreateLease, self).get_parser(prog_name)
parser.add_argument(
'--physical-reservation',
metavar="<min=int,max=int,hypervisor_properties=str,"
"resource_properties=str,before_end=str>",
action='append',
dest='physical_reservations',
help='Create a reservation for physical compute hosts. '
'Specify option multiple times to create multiple '
'reservations. '
'min: minimum number of hosts to reserve. '
'max: maximum number of hosts to reserve. '
'hypervisor_properties: JSON string, see doc. '
'resource_properties: JSON string, see doc. '
'before_end: JSON string, see doc. ',
default=[]
)
return parser
def args2body(self, parsed_args):
params = self._generate_params(parsed_args)
physical_reservations = []
for phys_res_str in parsed_args.physical_reservations:
err_msg = ("Invalid physical-reservation argument '%s'. "
@ -252,7 +321,7 @@ class CreateLease(command.CreateCommand):
"before_end=str>"
% phys_res_str)
defaults = CREATE_RESERVATION_KEYS["physical:host"]
phys_res_info = parse_params(phys_res_str, defaults)
phys_res_info = self._parse_params(phys_res_str, defaults, err_msg)
if not (phys_res_info['min'] and phys_res_info['max']):
raise exception.IncorrectLease(err_msg)
@ -284,61 +353,10 @@ class CreateLease(command.CreateCommand):
phys_res_info['resource_type'] = 'physical:host'
physical_reservations.append(phys_res_info)
if physical_reservations:
params['reservations'] += physical_reservations
reservations = []
for res_str in parsed_args.reservations:
err_msg = ("Invalid reservation argument '%s'. "
"Reservation arguments must be of the "
"form --reservation <key=value>"
% res_str)
if "physical:host" in res_str:
defaults = CREATE_RESERVATION_KEYS['physical:host']
elif "virtual:instance" in res_str:
defaults = CREATE_RESERVATION_KEYS['virtual:instance']
elif "virtual:floatingip" in res_str:
defaults = CREATE_RESERVATION_KEYS['virtual:floatingip']
else:
defaults = CREATE_RESERVATION_KEYS['others']
res_info = parse_params(res_str, defaults)
reservations.append(res_info)
if reservations:
params['reservations'] += reservations
if not params['reservations']:
raise exception.IncorrectLease
events = []
for event_str in parsed_args.events:
err_msg = ("Invalid event argument '%s'. "
"Event arguments must be of the "
"form --event <event_type=str,event_date=time>"
% event_str)
event_info = {"event_type": "", "event_date": ""}
for kv_str in event_str.split(","):
try:
k, v = kv_str.split("=", 1)
except ValueError:
raise exception.IncorrectLease(err_msg)
if k in event_info:
event_info[k] = v
else:
raise exception.IncorrectLease(err_msg)
if not event_info['event_type'] and not event_info['event_date']:
raise exception.IncorrectLease(err_msg)
event_date = event_info['event_date']
try:
date = datetime.datetime.strptime(event_date, '%Y-%m-%d %H:%M')
event_date = datetime.datetime.strftime(date, '%Y-%m-%d %H:%M')
event_info['event_date'] = event_date
except ValueError:
raise exception.IncorrectLease
events.append(event_info)
if events:
params['events'] = events
# We prepend the physical_reservations to preserve legacy order
# of reservations
params['reservations'] = physical_reservations \
+ params['reservations']
return params

View File

@ -15,6 +15,7 @@ msgpack-python==0.4.0
netaddr==0.7.18
netifaces==0.10.4
os-client-config==1.28.0
osc-lib==1.3.0
oslo.config==5.2.0
oslo.context==2.19.2
oslo.i18n==3.15.3

View File

@ -0,0 +1,5 @@
---
features:
- |
Add openstackclient plugin support, enabling blazar commands to be used
within the openstack CLI.

View File

@ -9,3 +9,4 @@ oslo.i18n>=3.15.3 # Apache-2.0
oslo.log>=3.36.0 # Apache-2.0
oslo.utils>=3.33.0 # Apache-2.0
keystoneauth1>=3.4.0 # Apache-2.0
osc-lib>=1.3.0 # Apache-2.0

View File

@ -30,3 +30,22 @@ packages =
[entry_points]
console_scripts =
blazar = blazarclient.shell:main
openstack.cli.extension =
reservation = blazarclient.osc.plugin
openstack.reservation.v1 =
reservation_floatingip_create = blazarclient.v1.shell_commands.floatingips:CreateFloatingIP
reservation_floatingip_delete = blazarclient.v1.shell_commands.floatingips:DeleteFloatingIP
reservation_floatingip_list = blazarclient.v1.shell_commands.floatingips:ListFloatingIPs
reservation_floatingip_show = blazarclient.v1.shell_commands.floatingips:ShowFloatingIP
reservation_host_create = blazarclient.v1.shell_commands.hosts:CreateHost
reservation_host_delete = blazarclient.v1.shell_commands.hosts:DeleteHost
reservation_host_list = blazarclient.v1.shell_commands.hosts:ListHosts
reservation_host_set = blazarclient.v1.shell_commands.hosts:UpdateHost
reservation_host_show = blazarclient.v1.shell_commands.hosts:ShowHost
reservation_lease_create = blazarclient.v1.shell_commands.leases:CreateLeaseBase
reservation_lease_delete = blazarclient.v1.shell_commands.leases:DeleteLease
reservation_lease_list = blazarclient.v1.shell_commands.leases:ListLeases
reservation_lease_set = blazarclient.v1.shell_commands.leases:UpdateLease
reservation_lease_show = blazarclient.v1.shell_commands.leases:ShowLease