Merge "Basic VIP management commands added to Nailgun CLI v1"

This commit is contained in:
Jenkins
2016-02-15 10:26:21 +00:00
committed by Gerrit Code Review
6 changed files with 444 additions and 28 deletions

View File

@@ -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(

View 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)
)

View File

@@ -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
)

View File

@@ -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)

View File

@@ -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",))

View 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())