Merge "Basic VIP management commands added to Nailgun CLI v1"
This commit is contained in:
@@ -42,34 +42,36 @@ from fuelclient.cli.actions.task import TaskAction
|
||||
from fuelclient.cli.actions.user import UserAction
|
||||
from fuelclient.cli.actions.plugins import PluginAction
|
||||
from fuelclient.cli.actions.fuelversion import FuelVersionAction
|
||||
from fuelclient.cli.actions.vip import VIPAction
|
||||
|
||||
actions_tuple = (
|
||||
ReleaseAction,
|
||||
RoleAction,
|
||||
EnvironmentAction,
|
||||
DeployChangesAction,
|
||||
NodeAction,
|
||||
DeploymentAction,
|
||||
ProvisioningAction,
|
||||
StopAction,
|
||||
ResetAction,
|
||||
SettingsAction,
|
||||
VmwareSettingsAction,
|
||||
NetworkAction,
|
||||
NetworkTemplateAction,
|
||||
TaskAction,
|
||||
SnapshotAction,
|
||||
EnvironmentAction,
|
||||
FuelVersionAction,
|
||||
GraphAction,
|
||||
HealthCheckAction,
|
||||
UserAction,
|
||||
PluginAction,
|
||||
NetworkAction,
|
||||
NetworkGroupAction,
|
||||
NetworkTemplateAction,
|
||||
NodeAction,
|
||||
NodeGroupAction,
|
||||
NotificationsAction,
|
||||
NotifyAction,
|
||||
TokenAction,
|
||||
GraphAction,
|
||||
FuelVersionAction,
|
||||
NetworkGroupAction,
|
||||
OpenstackConfigAction,
|
||||
PluginAction,
|
||||
ProvisioningAction,
|
||||
ReleaseAction,
|
||||
ResetAction,
|
||||
RoleAction,
|
||||
SettingsAction,
|
||||
SnapshotAction,
|
||||
StopAction,
|
||||
TaskAction,
|
||||
TokenAction,
|
||||
UserAction,
|
||||
VIPAction,
|
||||
VmwareSettingsAction,
|
||||
)
|
||||
|
||||
actions = dict(
|
||||
|
||||
76
fuelclient/cli/actions/vip.py
Normal file
76
fuelclient/cli/actions/vip.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# Copyright 2016 Mirantis, Inc.
|
||||
#
|
||||
# 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 fuelclient.cli.actions.base import Action
|
||||
import fuelclient.cli.arguments as Args
|
||||
from fuelclient.objects.environment import Environment
|
||||
|
||||
|
||||
class VIPAction(Action):
|
||||
"""Download or upload VIP settings of specific environments.
|
||||
"""
|
||||
action_name = "vip"
|
||||
acceptable_keys = ("id", "upload", "download", "network", "network-role",)
|
||||
|
||||
def __init__(self):
|
||||
super(VIPAction, self).__init__()
|
||||
self.args = (
|
||||
Args.get_env_arg(required=True),
|
||||
Args.get_upload_file_arg("Upload changed VIP configuration "
|
||||
"from given file"),
|
||||
Args.get_download_arg("Download VIP configuration"),
|
||||
Args.get_file_arg("Target file with vip data."),
|
||||
Args.get_ip_id_arg("IP address entity identifier"),
|
||||
Args.get_network_id_arg("Network identifier"),
|
||||
Args.get_network_role_arg("Network role string"),
|
||||
)
|
||||
self.flag_func_map = (
|
||||
("upload", self.upload),
|
||||
("download", self.download)
|
||||
)
|
||||
|
||||
def upload(self, params):
|
||||
"""To upload VIP configuration from some
|
||||
file for some environment:
|
||||
fuel --env 1 vip --upload vip.yaml
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
vips_data = env.read_vips_data_from_file(
|
||||
file_path=params.upload,
|
||||
serializer=self.serializer
|
||||
)
|
||||
env.set_vips_data(vips_data)
|
||||
print("VIP configuration uploaded.")
|
||||
|
||||
def download(self, params):
|
||||
"""To download VIP configuration in this
|
||||
file for some environment:
|
||||
fuel --env 1 vip --download --file vip.yaml
|
||||
where --file param is optional
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
vips_data = env.get_vips_data(
|
||||
ip_address_id=getattr(params, 'ip-address-id'),
|
||||
network=getattr(params, 'network'),
|
||||
network_role=getattr(params, 'network-role')
|
||||
)
|
||||
vips_data_file_path = env.write_vips_data_to_file(
|
||||
vips_data,
|
||||
file_path=params.file,
|
||||
serializer=self.serializer
|
||||
)
|
||||
print(
|
||||
"VIP configuration for environment with id={0}"
|
||||
" downloaded to {1}".format(env.id, vips_data_file_path)
|
||||
)
|
||||
@@ -631,3 +631,43 @@ def get_notify_topic_arg(help_msg):
|
||||
),
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_vip_arg(help_msg):
|
||||
return get_boolean_arg(
|
||||
"vip",
|
||||
flags=("--vip",),
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_ip_id_arg(help_msg):
|
||||
return get_int_arg(
|
||||
"ip-address-id",
|
||||
flags=("--ip-address-id",),
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_network_id_arg(help_msg):
|
||||
return get_int_arg(
|
||||
"network",
|
||||
flags=("--network",),
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_network_role_arg(help_msg):
|
||||
return get_str_arg(
|
||||
"network-role",
|
||||
flags=("--network-role",),
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_upload_file_arg(help_msg):
|
||||
return get_str_arg(
|
||||
"upload",
|
||||
flags=("-u", "--upload",),
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
@@ -16,9 +16,7 @@ from operator import attrgetter
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from fuelclient.cli.error import ActionException
|
||||
from fuelclient.cli.error import InvalidDirectoryException
|
||||
from fuelclient.cli.error import ServerDataException
|
||||
from fuelclient.cli import error
|
||||
from fuelclient.cli.serializers import listdir_without_extensions
|
||||
from fuelclient.objects.base import BaseObject
|
||||
from fuelclient.objects.task import DeployTask
|
||||
@@ -94,7 +92,7 @@ class Environment(BaseObject):
|
||||
def unassign_all(self):
|
||||
nodes = self.get_all_nodes()
|
||||
if not nodes:
|
||||
raise ActionException(
|
||||
raise error.ActionException(
|
||||
"Environment with id={0} doesn't have nodes to remove."
|
||||
.format(self.id)
|
||||
)
|
||||
@@ -181,12 +179,17 @@ class Environment(BaseObject):
|
||||
return (serializer or self.serializer).read_from_file(
|
||||
settings_file_path)
|
||||
|
||||
def _check_file_path(self, file_path):
|
||||
if not os.path.exists(file_path):
|
||||
raise error.InvalidFileException(
|
||||
"File '{0}' doesn't exist.".format(file_path))
|
||||
|
||||
def _check_dir(self, directory):
|
||||
if not os.path.exists(directory):
|
||||
raise InvalidDirectoryException(
|
||||
raise error.InvalidDirectoryException(
|
||||
"Directory '{0}' doesn't exist.".format(directory))
|
||||
if not os.path.isdir(directory):
|
||||
raise InvalidDirectoryException(
|
||||
raise error.InvalidDirectoryException(
|
||||
"Error: '{0}' is not a directory.".format(directory))
|
||||
|
||||
def read_vmware_settings_data(self, directory=os.curdir, serializer=None):
|
||||
@@ -311,7 +314,7 @@ class Environment(BaseObject):
|
||||
facts = self.connection.get_request(
|
||||
self._get_fact_default_url(fact_type, nodes=nodes))
|
||||
if not facts:
|
||||
raise ServerDataException(
|
||||
raise error.ServerDataException(
|
||||
"There is no {0} info for this "
|
||||
"environment!".format(fact_type)
|
||||
)
|
||||
@@ -321,7 +324,7 @@ class Environment(BaseObject):
|
||||
facts = self.connection.get_request(
|
||||
self._get_fact_url(fact_type, nodes=nodes))
|
||||
if not facts:
|
||||
raise ServerDataException(
|
||||
raise error.ServerDataException(
|
||||
"There is no {0} info for this "
|
||||
"environment!".format(fact_type)
|
||||
)
|
||||
@@ -513,3 +516,112 @@ class Environment(BaseObject):
|
||||
def spawn_vms(self):
|
||||
url = 'clusters/{0}/spawn_vms/'.format(self.id)
|
||||
return self.connection.put_request(url, {})
|
||||
|
||||
def _get_ip_addrs_url(self, vips=True, ip_addr_id=None):
|
||||
"""Generate ip address management url.
|
||||
|
||||
:param vips: manage vip properties of ip address
|
||||
:type vips: bool
|
||||
:param ip_addr_id: ip address identifier
|
||||
:type ip_addr_id: int
|
||||
:return: url
|
||||
:rtype: str
|
||||
"""
|
||||
ip_addr_url = "clusters/{0}/network_configuration/ips/".format(self.id)
|
||||
if ip_addr_id:
|
||||
ip_addr_url += '{0}/'.format(ip_addr_id)
|
||||
if vips:
|
||||
ip_addr_url += 'vips/'
|
||||
|
||||
return ip_addr_url
|
||||
|
||||
def get_default_vips_data_path(self):
|
||||
"""Get path where VIPs data is located.
|
||||
:return: path
|
||||
:rtype: str
|
||||
"""
|
||||
return os.path.join(
|
||||
os.path.abspath(os.curdir),
|
||||
"vips_{0}".format(self.id)
|
||||
)
|
||||
|
||||
def get_vips_data(self, ip_address_id=None, network=None,
|
||||
network_role=None):
|
||||
"""Get one or multiple vip data records.
|
||||
|
||||
:param ip_address_id: ip addr id could be specified to download single
|
||||
vip if no ip_addr_id specified multiple entities is
|
||||
returned respecting network and network_role
|
||||
filters
|
||||
:type ip_address_id: int
|
||||
:param network: network id could be specified to filter vips
|
||||
:type network: int
|
||||
:param network_role: network role could be specified to filter vips
|
||||
:type network_role: string
|
||||
:return: response JSON
|
||||
:rtype: list of dict
|
||||
"""
|
||||
params = {}
|
||||
if network:
|
||||
params['network'] = network
|
||||
if network_role:
|
||||
params['network-role'] = network_role
|
||||
|
||||
result = self.connection.get_request(
|
||||
self._get_ip_addrs_url(True, ip_addr_id=ip_address_id),
|
||||
params=params
|
||||
)
|
||||
if ip_address_id is not None: # single vip is returned
|
||||
# wrapping with list is required to respect case when administrator
|
||||
# is downloading vip address info to change it and upload
|
||||
# back. Uploading works only with lists of records.
|
||||
result = [result]
|
||||
return result
|
||||
|
||||
def write_vips_data_to_file(self, vips_data, serializer=None,
|
||||
file_path=None):
|
||||
"""Write VIP data to the given path.
|
||||
|
||||
:param vips_data: vip data
|
||||
:type vips_data: list of dict
|
||||
:param serializer: serializer
|
||||
:param file_path: path
|
||||
:type file_path: str
|
||||
:return: path to resulting file
|
||||
:rtype: str
|
||||
"""
|
||||
serializer = serializer or self.serializer
|
||||
|
||||
if file_path:
|
||||
return serializer.write_to_full_path(
|
||||
file_path,
|
||||
vips_data
|
||||
)
|
||||
else:
|
||||
return serializer.write_to_path(
|
||||
self.get_default_vips_data_path(),
|
||||
vips_data
|
||||
)
|
||||
|
||||
def read_vips_data_from_file(self, file_path=None, serializer=None):
|
||||
"""Read VIPs data from given path.
|
||||
|
||||
:param file_path: path
|
||||
:type file_path: str
|
||||
:param serializer: serializer object
|
||||
:type serializer: object
|
||||
:return: data
|
||||
:rtype: list|object
|
||||
"""
|
||||
self._check_file_path(file_path)
|
||||
return (serializer or self.serializer).read_from_file(file_path)
|
||||
|
||||
def set_vips_data(self, data):
|
||||
"""Sending VIPs data to the Nailgun API.
|
||||
|
||||
:param data: VIPs data
|
||||
:type data: list of dict
|
||||
:return: request result
|
||||
:rtype: object
|
||||
"""
|
||||
return self.connection.put_request(self._get_ip_addrs_url(), data)
|
||||
|
||||
@@ -181,7 +181,7 @@ class TestHandlers(base.BaseTestCase):
|
||||
actions = (
|
||||
"node", "stop", "deployment", "reset", "task", "network",
|
||||
"settings", "provisioning", "environment", "deploy-changes",
|
||||
"role", "release", "snapshot", "health"
|
||||
"role", "release", "snapshot", "health", "vip"
|
||||
)
|
||||
for action in actions:
|
||||
self.check_all_in_msg("{0} -h".format(action), ("Examples",))
|
||||
|
||||
186
fuelclient/tests/unit/v1/test_vip_action.py
Normal file
186
fuelclient/tests/unit/v1/test_vip_action.py
Normal file
@@ -0,0 +1,186 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 Mirantis, Inc.
|
||||
#
|
||||
# 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 mock
|
||||
import six
|
||||
import yaml
|
||||
|
||||
from fuelclient.tests.unit.v1 import base
|
||||
|
||||
ENV_OUTPUT = {
|
||||
"status": "new",
|
||||
"is_customized": False,
|
||||
"release_id": 2,
|
||||
"ui_settings": {
|
||||
"sort": [{"roles": "asc"}],
|
||||
"sort_by_labels": [],
|
||||
"search": "",
|
||||
"filter_by_labels": {},
|
||||
"filter": {},
|
||||
"view_mode": "standard"
|
||||
},
|
||||
"is_locked": False,
|
||||
"fuel_version": "8.0",
|
||||
"net_provider": "neutron",
|
||||
"mode": "ha_compact",
|
||||
"components": [],
|
||||
"pending_release_id": None,
|
||||
"changes": [
|
||||
{"node_id": None, "name": "attributes"},
|
||||
{"node_id": None, "name": "networks"},
|
||||
{"node_id": None, "name": "vmware_attributes"}],
|
||||
"id": 6, "name": "test_not_deployed"}
|
||||
|
||||
|
||||
MANY_VIPS_YAML = '''- id: 5
|
||||
network: 3
|
||||
node: null
|
||||
ip_addr: 192.169.1.33
|
||||
vip_name: public
|
||||
vip_namespace: haproxy
|
||||
is_user_defined: false
|
||||
- id: 6
|
||||
network: 3
|
||||
node: null
|
||||
ip_addr: 192.169.1.34
|
||||
vip_namespace: null
|
||||
vip_name: private
|
||||
is_user_defined: true
|
||||
'''
|
||||
|
||||
ONE_VIP_YAML = '''
|
||||
- id: 5
|
||||
network: 3
|
||||
node: null
|
||||
ip_addr: 192.169.1.33
|
||||
vip_name: public
|
||||
vip_namespace: haproxy
|
||||
is_user_defined: false
|
||||
'''
|
||||
|
||||
|
||||
@mock.patch('fuelclient.cli.serializers.open', create=True)
|
||||
@mock.patch('fuelclient.cli.actions.base.os')
|
||||
class TestVIPActions(base.UnitTestCase):
|
||||
|
||||
def test_env_vips_download(self, mos, mopen):
|
||||
self.m_request.get('/api/v1/clusters/1/', json=ENV_OUTPUT)
|
||||
url = '/api/v1/clusters/1/network_configuration/ips/vips/'
|
||||
get_request = self.m_request.get(
|
||||
url,
|
||||
json=yaml.load(MANY_VIPS_YAML))
|
||||
self.execute('fuel vip --env 1 --download'.split())
|
||||
self.assertTrue(get_request.called)
|
||||
self.assertEqual(1, mopen().__enter__().write.call_count)
|
||||
self.assertEqual(
|
||||
yaml.safe_load(MANY_VIPS_YAML),
|
||||
yaml.safe_load(mopen().__enter__().write.call_args[0][0]),
|
||||
)
|
||||
|
||||
def test_env_vips_download_with_network_id(self, mos, mopen):
|
||||
self.m_request.get('/api/v1/clusters/1/', json=ENV_OUTPUT)
|
||||
url = '/api/v1/clusters/1/network_configuration/ips/vips/'
|
||||
get_request = self.m_request.get(
|
||||
url,
|
||||
json=yaml.load(MANY_VIPS_YAML))
|
||||
self.execute('fuel vip --env 1 --network 3 --download'.split())
|
||||
self.assertTrue(get_request.called)
|
||||
self.assertEqual(1, mopen().__enter__().write.call_count)
|
||||
self.assertEqual(
|
||||
yaml.safe_load(MANY_VIPS_YAML),
|
||||
yaml.safe_load(mopen().__enter__().write.call_args[0][0]),
|
||||
)
|
||||
|
||||
def test_env_vips_download_with_network_role(self, mos, mopen):
|
||||
self.m_request.get('/api/v1/clusters/1/', json=ENV_OUTPUT)
|
||||
url = '/api/v1/clusters/1/network_configuration/ips/vips/'
|
||||
get_request = self.m_request.get(
|
||||
url,
|
||||
json=yaml.load(MANY_VIPS_YAML))
|
||||
self.execute(
|
||||
'fuel vip --env 1 --network-role some/role --download'.split())
|
||||
self.assertTrue(get_request.called)
|
||||
self.assertEqual(1, mopen().__enter__().write.call_count)
|
||||
self.assertEqual(
|
||||
yaml.safe_load(MANY_VIPS_YAML),
|
||||
yaml.safe_load(mopen().__enter__().write.call_args[0][0]),
|
||||
)
|
||||
|
||||
def test_env_single_vip_download(self, mos, mopen):
|
||||
self.m_request.get('/api/v1/clusters/1/', json=ENV_OUTPUT)
|
||||
url = '/api/v1/clusters/1/network_configuration/ips/5/vips/'
|
||||
get_request = self.m_request.get(
|
||||
url,
|
||||
json=yaml.safe_load(ONE_VIP_YAML)[0]
|
||||
)
|
||||
self.execute('fuel vip --env 1 --ip-address-id 5 --download'.split())
|
||||
|
||||
self.assertTrue(get_request.called)
|
||||
self.assertEqual(1, mopen().__enter__().write.call_count)
|
||||
|
||||
self.assertEqual(
|
||||
yaml.safe_load(ONE_VIP_YAML),
|
||||
yaml.safe_load(mopen().__enter__().write.call_args[0][0]),
|
||||
)
|
||||
self.assertIn(
|
||||
'vips_1.yaml',
|
||||
mopen.call_args_list[0][0][0]
|
||||
)
|
||||
|
||||
def test_env_single_vip_download_to_file(self, mos, mopen):
|
||||
self.m_request.get('/api/v1/clusters/1/', json=ENV_OUTPUT)
|
||||
url = '/api/v1/clusters/1/network_configuration/ips/5/vips/'
|
||||
get_request = self.m_request.get(
|
||||
url,
|
||||
json=yaml.safe_load(ONE_VIP_YAML)[0]
|
||||
)
|
||||
self.execute('fuel vip --env 1 --ip-address-id 5 '
|
||||
'--download --file vips.yaml'.split())
|
||||
|
||||
self.assertTrue(get_request.called)
|
||||
self.assertEqual(1, mopen().__enter__().write.call_count)
|
||||
|
||||
self.assertEqual(
|
||||
yaml.safe_load(ONE_VIP_YAML),
|
||||
yaml.safe_load(mopen().__enter__().write.call_args[0][0]),
|
||||
)
|
||||
self.assertEqual(
|
||||
'vips.yaml',
|
||||
mopen.call_args_list[0][0][0]
|
||||
)
|
||||
|
||||
def test_vips_upload(self, mos, mopen):
|
||||
url = '/api/v1/clusters/1/network_configuration/ips/vips/'
|
||||
mopen().__enter__().read.return_value = MANY_VIPS_YAML
|
||||
self.m_request.get('/api/v1/clusters/1/', json=ENV_OUTPUT)
|
||||
request_put = self.m_request.put(url, json={})
|
||||
with mock.patch('fuelclient.objects.environment.os') as env_os:
|
||||
env_os.path.exists.return_value = True
|
||||
self.execute('fuel vip --env 1 --upload vips_1.yaml'.split())
|
||||
self.assertEqual(env_os.path.exists.call_count, 1)
|
||||
self.assertEqual(request_put.call_count, 1)
|
||||
self.assertIn(url, request_put.last_request.url)
|
||||
|
||||
def test_vips_upload_bad_path(self, mos, mopen):
|
||||
with mock.patch('sys.stderr', new=six.moves.cStringIO()) as mstderr:
|
||||
with mock.patch('fuelclient.objects.environment.os') as env_os:
|
||||
env_os.path.exists.return_value = False
|
||||
self.assertRaises(
|
||||
SystemExit,
|
||||
self.execute,
|
||||
'fuel vip --env 1 --upload vips_1.yaml'.split()
|
||||
)
|
||||
self.assertIn("doesn't exist", mstderr.getvalue())
|
||||
Reference in New Issue
Block a user