mirror of
https://github.com/valitydev/salt.git
synced 2024-11-08 09:23:56 +00:00
Merge branch 'develop' into 41510_mount_opts
This commit is contained in:
commit
892d459f01
@ -51,6 +51,19 @@ New NaCl Renderer
|
||||
|
||||
A new renderer has been added for encrypted data.
|
||||
|
||||
New support for Cisco UCS Chassis
|
||||
---------------------------------
|
||||
|
||||
The salt proxy minion now allows for control of Cisco USC chassis. See
|
||||
the `cimc` modules for details.
|
||||
|
||||
New salt-ssh roster
|
||||
-------------------
|
||||
|
||||
A new roster has been added that allows users to pull in a list of hosts
|
||||
for salt-ssh targeting from a ~/.ssh configuration. For full details,
|
||||
please see the `sshconfig` roster.
|
||||
|
||||
New GitFS Features
|
||||
------------------
|
||||
|
||||
|
@ -44,9 +44,6 @@ from salt.exceptions import (
|
||||
SaltCloudSystemExit
|
||||
)
|
||||
|
||||
# Import Salt-Cloud Libs
|
||||
import salt.utils.cloud
|
||||
|
||||
# Get logging started
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -1193,7 +1190,7 @@ def list_nodes_select(call=None):
|
||||
'''
|
||||
Return a list of the VMs that are on the provider, with select fields.
|
||||
'''
|
||||
return salt.utils.cloud.list_nodes_select(
|
||||
return __utils__['cloud.list_nodes_select'](
|
||||
list_nodes_full(), __opts__['query.selection'], call,
|
||||
)
|
||||
|
||||
@ -1503,7 +1500,7 @@ def _query(action=None,
|
||||
if LASTCALL >= now:
|
||||
time.sleep(ratelimit_sleep)
|
||||
|
||||
result = salt.utils.http.query(
|
||||
result = __utils__['http.query'](
|
||||
url,
|
||||
method,
|
||||
params=args,
|
||||
|
44
salt/config/schemas/esxcluster.py
Normal file
44
salt/config/schemas/esxcluster.py
Normal file
@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
:codeauthor: :email:`Alexandru Bleotu (alexandru.bleotu@morganstanley.com)`
|
||||
|
||||
|
||||
salt.config.schemas.esxcluster
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
ESX Cluster configuration schemas
|
||||
'''
|
||||
|
||||
# Import Python libs
|
||||
from __future__ import absolute_import
|
||||
|
||||
# Import Salt libs
|
||||
from salt.utils.schema import (Schema,
|
||||
ArrayItem,
|
||||
IntegerItem,
|
||||
StringItem)
|
||||
|
||||
|
||||
class EsxclusterProxySchema(Schema):
|
||||
'''
|
||||
Schema of the esxcluster proxy input
|
||||
'''
|
||||
|
||||
title = 'Esxcluster Proxy Schema'
|
||||
description = 'Esxcluster proxy schema'
|
||||
additional_properties = False
|
||||
proxytype = StringItem(required=True,
|
||||
enum=['esxcluster'])
|
||||
vcenter = StringItem(required=True, pattern=r'[^\s]+')
|
||||
datacenter = StringItem(required=True)
|
||||
cluster = StringItem(required=True)
|
||||
mechanism = StringItem(required=True, enum=['userpass', 'sspi'])
|
||||
username = StringItem()
|
||||
passwords = ArrayItem(min_items=1,
|
||||
items=StringItem(),
|
||||
unique_items=True)
|
||||
# TODO Should be changed when anyOf is supported for schemas
|
||||
domain = StringItem()
|
||||
principal = StringItem()
|
||||
protocol = StringItem()
|
||||
port = IntegerItem(minimum=1)
|
38
salt/grains/cimc.py
Normal file
38
salt/grains/cimc.py
Normal file
@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
Generate baseline proxy minion grains for cimc hosts.
|
||||
|
||||
'''
|
||||
|
||||
# Import Python Libs
|
||||
from __future__ import absolute_import
|
||||
import logging
|
||||
|
||||
# Import Salt Libs
|
||||
import salt.utils.platform
|
||||
import salt.proxy.cimc
|
||||
|
||||
__proxyenabled__ = ['cimc']
|
||||
__virtualname__ = 'cimc'
|
||||
|
||||
log = logging.getLogger(__file__)
|
||||
|
||||
GRAINS_CACHE = {'os_family': 'Cisco UCS'}
|
||||
|
||||
|
||||
def __virtual__():
|
||||
try:
|
||||
if salt.utils.platform.is_proxy() and __opts__['proxy']['proxytype'] == 'cimc':
|
||||
return __virtualname__
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def cimc(proxy=None):
|
||||
if not proxy:
|
||||
return {}
|
||||
if proxy['cimc.initialized']() is False:
|
||||
return {}
|
||||
return {'cimc': proxy['cimc.grains']()}
|
@ -99,7 +99,7 @@ def __virtual__():
|
||||
'''
|
||||
Confirm this module is on a Debian based system
|
||||
'''
|
||||
if __grains__.get('os_family') in ('Kali', 'Debian', 'neon'):
|
||||
if __grains__.get('os_family') in ('Kali', 'Debian', 'neon', 'Deepin'):
|
||||
return __virtualname__
|
||||
elif __grains__.get('os_family', False) == 'Cumulus':
|
||||
return __virtualname__
|
||||
|
710
salt/modules/cimc.py
Normal file
710
salt/modules/cimc.py
Normal file
@ -0,0 +1,710 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
Module to provide Cisco UCS compatibility to Salt.
|
||||
|
||||
:codeauthor: :email:`Spencer Ervin <spencer_ervin@hotmail.com>`
|
||||
:maturity: new
|
||||
:depends: none
|
||||
:platform: unix
|
||||
|
||||
|
||||
Configuration
|
||||
=============
|
||||
This module accepts connection configuration details either as
|
||||
parameters, or as configuration settings in pillar as a Salt proxy.
|
||||
Options passed into opts will be ignored if options are passed into pillar.
|
||||
|
||||
.. seealso::
|
||||
:prox:`Cisco UCS Proxy Module <salt.proxy.cimc>`
|
||||
|
||||
About
|
||||
=====
|
||||
This execution module was designed to handle connections to a Cisco UCS server. This module adds support to send
|
||||
connections directly to the device through the rest API.
|
||||
|
||||
'''
|
||||
|
||||
# Import Python Libs
|
||||
from __future__ import absolute_import
|
||||
import logging
|
||||
|
||||
# Import Salt Libs
|
||||
import salt.utils.platform
|
||||
import salt.proxy.cimc
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
__virtualname__ = 'cimc'
|
||||
|
||||
|
||||
def __virtual__():
|
||||
'''
|
||||
Will load for the cimc proxy minions.
|
||||
'''
|
||||
try:
|
||||
if salt.utils.platform.is_proxy() and \
|
||||
__opts__['proxy']['proxytype'] == 'cimc':
|
||||
return __virtualname__
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return False, 'The cimc execution module can only be loaded for cimc proxy minions.'
|
||||
|
||||
|
||||
def activate_backup_image(reset=False):
|
||||
'''
|
||||
Activates the firmware backup image.
|
||||
|
||||
CLI Example:
|
||||
|
||||
Args:
|
||||
reset(bool): Reset the CIMC device on activate.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.activate_backup_image
|
||||
salt '*' cimc.activate_backup_image reset=True
|
||||
|
||||
'''
|
||||
|
||||
dn = "sys/rack-unit-1/mgmt/fw-boot-def/bootunit-combined"
|
||||
|
||||
r = "no"
|
||||
|
||||
if reset is True:
|
||||
r = "yes"
|
||||
|
||||
inconfig = """<firmwareBootUnit dn='sys/rack-unit-1/mgmt/fw-boot-def/bootunit-combined'
|
||||
adminState='trigger' image='backup' resetOnActivate='{0}' />""".format(r)
|
||||
|
||||
ret = __proxy__['cimc.set_config_modify'](dn, inconfig, False)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def create_user(uid=None, username=None, password=None, priv=None):
|
||||
'''
|
||||
Create a CIMC user with username and password.
|
||||
|
||||
Args:
|
||||
uid(int): The user ID slot to create the user account in.
|
||||
|
||||
username(str): The name of the user.
|
||||
|
||||
password(str): The clear text password of the user.
|
||||
|
||||
priv(str): The privilege level of the user.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.create_user 11 username=admin password=foobar priv=admin
|
||||
|
||||
'''
|
||||
|
||||
if not uid:
|
||||
raise salt.exceptions.CommandExecutionError("The user ID must be specified.")
|
||||
|
||||
if not username:
|
||||
raise salt.exceptions.CommandExecutionError("The username must be specified.")
|
||||
|
||||
if not password:
|
||||
raise salt.exceptions.CommandExecutionError("The password must be specified.")
|
||||
|
||||
if not priv:
|
||||
raise salt.exceptions.CommandExecutionError("The privilege level must be specified.")
|
||||
|
||||
dn = "sys/user-ext/user-{0}".format(uid)
|
||||
|
||||
inconfig = """<aaaUser id="{0}" accountStatus="active" name="{1}" priv="{2}"
|
||||
pwd="{3}" dn="sys/user-ext/user-{0}"/>""".format(uid,
|
||||
username,
|
||||
priv,
|
||||
password)
|
||||
|
||||
ret = __proxy__['cimc.set_config_modify'](dn, inconfig, False)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_bios_defaults():
|
||||
'''
|
||||
Get the default values of BIOS tokens.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_bios_defaults
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('biosPlatformDefaults', True)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_bios_settings():
|
||||
'''
|
||||
Get the C240 server BIOS token values.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_bios_settings
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('biosSettings', True)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_boot_order():
|
||||
'''
|
||||
Retrieves the configured boot order table.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_boot_order
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('lsbootDef', True)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_cpu_details():
|
||||
'''
|
||||
Get the CPU product ID details.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_cpu_details
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('pidCatalogCpu', True)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_disks():
|
||||
'''
|
||||
Get the HDD product ID details.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_disks
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('pidCatalogHdd', True)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_ethernet_interfaces():
|
||||
'''
|
||||
Get the adapter Ethernet interface details.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_ethernet_interfaces
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('adaptorHostEthIf', True)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_fibre_channel_interfaces():
|
||||
'''
|
||||
Get the adapter fibre channel interface details.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_fibre_channel_interfaces
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('adaptorHostFcIf', True)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_firmware():
|
||||
'''
|
||||
Retrieves the current running firmware versions of server components.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_firmware
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('firmwareRunning', False)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_ldap():
|
||||
'''
|
||||
Retrieves LDAP server details.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_ldap
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('aaaLdap', True)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_management_interface():
|
||||
'''
|
||||
Retrieve the management interface details.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_management_interface
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('mgmtIf', False)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_memory_token():
|
||||
'''
|
||||
Get the memory RAS BIOS token.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_memory_token
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('biosVfSelectMemoryRASConfiguration', False)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_memory_unit():
|
||||
'''
|
||||
Get the IMM/Memory unit product ID details.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_memory_unit
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('pidCatalogDimm', True)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_network_adapters():
|
||||
'''
|
||||
Get the list of network adapaters and configuration details.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_network_adapters
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('networkAdapterEthIf', True)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_ntp():
|
||||
'''
|
||||
Retrieves the current running NTP configuration.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_ntp
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('commNtpProvider', False)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_pci_adapters():
|
||||
'''
|
||||
Get the PCI adapter product ID details.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_disks
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('pidCatalogPCIAdapter', True)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_power_supplies():
|
||||
'''
|
||||
Retrieves the power supply unit details.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_power_supplies
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('equipmentPsu', False)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_snmp_config():
|
||||
'''
|
||||
Get the snmp configuration details.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_snmp_config
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('commSnmp', False)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_syslog():
|
||||
'''
|
||||
Get the Syslog client-server details.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_syslog
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('commSyslogClient', False)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_system_info():
|
||||
'''
|
||||
Get the system information.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_system_info
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('computeRackUnit', False)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_users():
|
||||
'''
|
||||
Get the CIMC users.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_users
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('aaaUser', False)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_vic_adapters():
|
||||
'''
|
||||
Get the VIC adapter general profile details.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_vic_adapters
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('adaptorGenProfile', True)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_vic_uplinks():
|
||||
'''
|
||||
Get the VIC adapter uplink port details.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.get_vic_uplinks
|
||||
|
||||
'''
|
||||
ret = __proxy__['cimc.get_config_resolver_class']('adaptorExtEthIf', True)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def mount_share(name=None,
|
||||
remote_share=None,
|
||||
remote_file=None,
|
||||
mount_type="nfs",
|
||||
username=None,
|
||||
password=None):
|
||||
'''
|
||||
Mounts a remote file through a remote share. Currently, this feature is supported in version 1.5 or greater.
|
||||
The remote share can be either NFS, CIFS, or WWW.
|
||||
|
||||
Some of the advantages of CIMC Mounted vMedia include:
|
||||
Communication between mounted media and target stays local (inside datacenter)
|
||||
Media mounts can be scripted/automated
|
||||
No vKVM requirements for media connection
|
||||
Multiple share types supported
|
||||
Connections supported through all CIMC interfaces
|
||||
|
||||
Note: CIMC Mounted vMedia is enabled through BIOS configuration.
|
||||
|
||||
Args:
|
||||
name(str): The name of the volume on the CIMC device.
|
||||
|
||||
remote_share(str): The file share link that will be used to mount the share. This can be NFS, CIFS, or WWW. This
|
||||
must be the directory path and not the full path to the remote file.
|
||||
|
||||
remote_file(str): The name of the remote file to mount. It must reside within remote_share.
|
||||
|
||||
mount_type(str): The type of share to mount. Valid options are nfs, cifs, and www.
|
||||
|
||||
username(str): An optional requirement to pass credentials to the remote share. If not provided, an
|
||||
unauthenticated connection attempt will be made.
|
||||
|
||||
password(str): An optional requirement to pass a password to the remote share. If not provided, an
|
||||
unauthenticated connection attempt will be made.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.mount_share name=WIN7 remote_share=10.xxx.27.xxx:/nfs remote_file=sl1huu.iso
|
||||
|
||||
salt '*' cimc.mount_share name=WIN7 remote_share=10.xxx.27.xxx:/nfs remote_file=sl1huu.iso username=bob password=badpassword
|
||||
|
||||
'''
|
||||
|
||||
if not name:
|
||||
raise salt.exceptions.CommandExecutionError("The share name must be specified.")
|
||||
|
||||
if not remote_share:
|
||||
raise salt.exceptions.CommandExecutionError("The remote share path must be specified.")
|
||||
|
||||
if not remote_file:
|
||||
raise salt.exceptions.CommandExecutionError("The remote file name must be specified.")
|
||||
|
||||
if username and password:
|
||||
mount_options = " mountOptions='username={0},password={1}'".format(username, password)
|
||||
else:
|
||||
mount_options = ""
|
||||
|
||||
dn = 'sys/svc-ext/vmedia-svc/vmmap-{0}'.format(name)
|
||||
inconfig = """<commVMediaMap dn='sys/svc-ext/vmedia-svc/vmmap-{0}' map='{1}'{2}
|
||||
remoteFile='{3}' remoteShare='{4}' status='created'
|
||||
volumeName='Win12' />""".format(name, mount_type, mount_options, remote_file, remote_share)
|
||||
|
||||
ret = __proxy__['cimc.set_config_modify'](dn, inconfig, False)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def reboot():
|
||||
'''
|
||||
Power cycling the server.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.reboot
|
||||
|
||||
'''
|
||||
|
||||
dn = "sys/rack-unit-1"
|
||||
|
||||
inconfig = """<computeRackUnit adminPower="cycle-immediate" dn="sys/rack-unit-1"></computeRackUnit>"""
|
||||
|
||||
ret = __proxy__['cimc.set_config_modify'](dn, inconfig, False)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def set_ntp_server(server1='', server2='', server3='', server4=''):
|
||||
'''
|
||||
Sets the NTP servers configuration. This will also enable the client NTP service.
|
||||
|
||||
Args:
|
||||
server1(str): The first IP address or FQDN of the NTP servers.
|
||||
|
||||
server2(str): The second IP address or FQDN of the NTP servers.
|
||||
|
||||
server3(str): The third IP address or FQDN of the NTP servers.
|
||||
|
||||
server4(str): The fourth IP address or FQDN of the NTP servers.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.set_ntp_server 10.10.10.1
|
||||
|
||||
salt '*' cimc.set_ntp_server 10.10.10.1 foo.bar.com
|
||||
|
||||
'''
|
||||
|
||||
dn = "sys/svc-ext/ntp-svc"
|
||||
inconfig = """<commNtpProvider dn="sys/svc-ext/ntp-svc" ntpEnable="yes" ntpServer1="{0}" ntpServer2="{1}"
|
||||
ntpServer3="{2}" ntpServer4="{3}"/>""".format(server1, server2, server3, server4)
|
||||
|
||||
ret = __proxy__['cimc.set_config_modify'](dn, inconfig, False)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def set_syslog_server(server=None, type="primary"):
|
||||
'''
|
||||
Set the SYSLOG server on the host.
|
||||
|
||||
Args:
|
||||
server(str): The hostname or IP address of the SYSLOG server.
|
||||
|
||||
type(str): Specifies the type of SYSLOG server. This can either be primary (default) or secondary.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.set_syslog_server foo.bar.com
|
||||
|
||||
salt '*' cimc.set_syslog_server foo.bar.com primary
|
||||
|
||||
salt '*' cimc.set_syslog_server foo.bar.com secondary
|
||||
|
||||
'''
|
||||
|
||||
if not server:
|
||||
raise salt.exceptions.CommandExecutionError("The SYSLOG server must be specified.")
|
||||
|
||||
if type == "primary":
|
||||
dn = "sys/svc-ext/syslog/client-primary"
|
||||
inconfig = """<commSyslogClient name='primary' adminState='enabled' hostname='{0}'
|
||||
dn='sys/svc-ext/syslog/client-primary'> </commSyslogClient>""".format(server)
|
||||
elif type == "secondary":
|
||||
dn = "sys/svc-ext/syslog/client-secondary"
|
||||
inconfig = """<commSyslogClient name='secondary' adminState='enabled' hostname='{0}'
|
||||
dn='sys/svc-ext/syslog/client-secondary'> </commSyslogClient>""".format(server)
|
||||
else:
|
||||
raise salt.exceptions.CommandExecutionError("The SYSLOG type must be either primary or secondary.")
|
||||
|
||||
ret = __proxy__['cimc.set_config_modify'](dn, inconfig, False)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def tftp_update_bios(server=None, path=None):
|
||||
'''
|
||||
Update the BIOS firmware through TFTP.
|
||||
|
||||
Args:
|
||||
server(str): The IP address or hostname of the TFTP server.
|
||||
|
||||
path(str): The TFTP path and filename for the BIOS image.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.tftp_update_bios foo.bar.com HP-SL2.cap
|
||||
|
||||
'''
|
||||
|
||||
if not server:
|
||||
raise salt.exceptions.CommandExecutionError("The server name must be specified.")
|
||||
|
||||
if not path:
|
||||
raise salt.exceptions.CommandExecutionError("The TFTP path must be specified.")
|
||||
|
||||
dn = "sys/rack-unit-1/bios/fw-updatable"
|
||||
|
||||
inconfig = """<firmwareUpdatable adminState='trigger' dn='sys/rack-unit-1/bios/fw-updatable'
|
||||
protocol='tftp' remoteServer='{0}' remotePath='{1}'
|
||||
type='blade-bios' />""".format(server, path)
|
||||
|
||||
ret = __proxy__['cimc.set_config_modify'](dn, inconfig, False)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def tftp_update_cimc(server=None, path=None):
|
||||
'''
|
||||
Update the CIMC firmware through TFTP.
|
||||
|
||||
Args:
|
||||
server(str): The IP address or hostname of the TFTP server.
|
||||
|
||||
path(str): The TFTP path and filename for the CIMC image.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' cimc.tftp_update_cimc foo.bar.com HP-SL2.bin
|
||||
|
||||
'''
|
||||
|
||||
if not server:
|
||||
raise salt.exceptions.CommandExecutionError("The server name must be specified.")
|
||||
|
||||
if not path:
|
||||
raise salt.exceptions.CommandExecutionError("The TFTP path must be specified.")
|
||||
|
||||
dn = "sys/rack-unit-1/mgmt/fw-updatable"
|
||||
|
||||
inconfig = """<firmwareUpdatable adminState='trigger' dn='sys/rack-unit-1/mgmt/fw-updatable'
|
||||
protocol='tftp' remoteServer='{0}' remotePath='{1}'
|
||||
type='blade-controller' />""".format(server, path)
|
||||
|
||||
ret = __proxy__['cimc.set_config_modify'](dn, inconfig, False)
|
||||
|
||||
return ret
|
29
salt/modules/esxcluster.py
Normal file
29
salt/modules/esxcluster.py
Normal file
@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
Module used to access the esxcluster proxy connection methods
|
||||
'''
|
||||
from __future__ import absolute_import
|
||||
|
||||
# Import python libs
|
||||
import logging
|
||||
import salt.utils.platform
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
__proxyenabled__ = ['esxcluster']
|
||||
# Define the module's virtual name
|
||||
__virtualname__ = 'esxcluster'
|
||||
|
||||
|
||||
def __virtual__():
|
||||
'''
|
||||
Only work on proxy
|
||||
'''
|
||||
if salt.utils.platform.is_proxy():
|
||||
return __virtualname__
|
||||
return (False, 'Must be run on a proxy minion')
|
||||
|
||||
|
||||
def get_details():
|
||||
return __proxy__['esxcluster.get_details']()
|
@ -195,7 +195,7 @@ else:
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
__virtualname__ = 'vsphere'
|
||||
__proxyenabled__ = ['esxi', 'esxdatacenter']
|
||||
__proxyenabled__ = ['esxi', 'esxcluster', 'esxdatacenter']
|
||||
|
||||
|
||||
def __virtual__():
|
||||
@ -227,6 +227,8 @@ def _get_proxy_connection_details():
|
||||
proxytype = get_proxy_type()
|
||||
if proxytype == 'esxi':
|
||||
details = __salt__['esxi.get_details']()
|
||||
elif proxytype == 'esxcluster':
|
||||
details = __salt__['esxcluster.get_details']()
|
||||
elif proxytype == 'esxdatacenter':
|
||||
details = __salt__['esxdatacenter.get_details']()
|
||||
else:
|
||||
@ -267,7 +269,7 @@ def gets_service_instance_via_proxy(fn):
|
||||
proxy details and passes the connection (vim.ServiceInstance) to
|
||||
the decorated function.
|
||||
|
||||
Supported proxies: esxi, esxdatacenter.
|
||||
Supported proxies: esxi, esxcluster, esxdatacenter.
|
||||
|
||||
Notes:
|
||||
1. The decorated function must have a ``service_instance`` parameter
|
||||
@ -354,7 +356,7 @@ def gets_service_instance_via_proxy(fn):
|
||||
|
||||
|
||||
@depends(HAS_PYVMOMI)
|
||||
@supports_proxies('esxi', 'esxdatacenter')
|
||||
@supports_proxies('esxi', 'esxcluster', 'esxdatacenter')
|
||||
def get_service_instance_via_proxy(service_instance=None):
|
||||
'''
|
||||
Returns a service instance to the proxied endpoint (vCenter/ESXi host).
|
||||
@ -374,7 +376,7 @@ def get_service_instance_via_proxy(service_instance=None):
|
||||
|
||||
|
||||
@depends(HAS_PYVMOMI)
|
||||
@supports_proxies('esxi', 'esxdatacenter')
|
||||
@supports_proxies('esxi', 'esxcluster', 'esxdatacenter')
|
||||
def disconnect(service_instance):
|
||||
'''
|
||||
Disconnects from a vCenter or ESXi host
|
||||
@ -1909,7 +1911,7 @@ def get_vsan_eligible_disks(host, username, password, protocol=None, port=None,
|
||||
|
||||
|
||||
@depends(HAS_PYVMOMI)
|
||||
@supports_proxies('esxi', 'esxdatacenter')
|
||||
@supports_proxies('esxi', 'esxcluster', 'esxdatacenter')
|
||||
@gets_service_instance_via_proxy
|
||||
def test_vcenter_connection(service_instance=None):
|
||||
'''
|
||||
@ -3598,7 +3600,7 @@ def vsan_enable(host, username, password, protocol=None, port=None, host_names=N
|
||||
|
||||
|
||||
@depends(HAS_PYVMOMI)
|
||||
@supports_proxies('esxdatacenter')
|
||||
@supports_proxies('esxdatacenter', 'esxcluster')
|
||||
@gets_service_instance_via_proxy
|
||||
def list_datacenters_via_proxy(datacenter_names=None, service_instance=None):
|
||||
'''
|
||||
@ -4294,3 +4296,14 @@ def _get_esxdatacenter_proxy_details():
|
||||
return det.get('vcenter'), det.get('username'), det.get('password'), \
|
||||
det.get('protocol'), det.get('port'), det.get('mechanism'), \
|
||||
det.get('principal'), det.get('domain'), det.get('datacenter')
|
||||
|
||||
|
||||
def _get_esxcluster_proxy_details():
|
||||
'''
|
||||
Returns the running esxcluster's proxy details
|
||||
'''
|
||||
det = __salt__['esxcluster.get_details']()
|
||||
return det.get('vcenter'), det.get('username'), det.get('password'), \
|
||||
det.get('protocol'), det.get('port'), det.get('mechanism'), \
|
||||
det.get('principal'), det.get('domain'), det.get('datacenter'), \
|
||||
det.get('cluster')
|
||||
|
290
salt/proxy/cimc.py
Normal file
290
salt/proxy/cimc.py
Normal file
@ -0,0 +1,290 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
|
||||
Proxy Minion interface module for managing Cisco Integrated Management Controller devices.
|
||||
|
||||
:codeauthor: :email:`Spencer Ervin <spencer_ervin@hotmail.com>`
|
||||
:maturity: new
|
||||
:depends: none
|
||||
:platform: unix
|
||||
|
||||
This proxy minion enables Cisco Integrated Management Controller devices (hereafter referred to
|
||||
as simply 'cimc' devices to be treated individually like a Salt Minion.
|
||||
|
||||
The cimc proxy leverages the XML API functionality on the Cisco Integrated Management Controller.
|
||||
The Salt proxy must have access to the cimc on HTTPS (tcp/443).
|
||||
|
||||
More in-depth conceptual reading on Proxy Minions can be found in the
|
||||
:ref:`Proxy Minion <proxy-minion>` section of Salt's
|
||||
documentation.
|
||||
|
||||
|
||||
Configuration
|
||||
=============
|
||||
To use this integration proxy module, please configure the following:
|
||||
|
||||
Pillar
|
||||
------
|
||||
|
||||
Proxy minions get their configuration from Salt's Pillar. Every proxy must
|
||||
have a stanza in Pillar and a reference in the Pillar top-file that matches
|
||||
the ID.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
proxy:
|
||||
proxytype: cimc
|
||||
host: <ip or dns name of cimc host>
|
||||
username: <cimc username>
|
||||
password: <cimc password>
|
||||
|
||||
proxytype
|
||||
^^^^^^^^^
|
||||
The ``proxytype`` key and value pair is critical, as it tells Salt which
|
||||
interface to load from the ``proxy`` directory in Salt's install hierarchy,
|
||||
or from ``/srv/salt/_proxy`` on the Salt Master (if you have created your
|
||||
own proxy module, for example). To use this cimc Proxy Module, set this to
|
||||
``cimc``.
|
||||
|
||||
host
|
||||
^^^^
|
||||
The location, or ip/dns, of the cimc host. Required.
|
||||
|
||||
username
|
||||
^^^^^^^^
|
||||
The username used to login to the cimc host. Required.
|
||||
|
||||
password
|
||||
^^^^^^^^
|
||||
The password used to login to the cimc host. Required.
|
||||
|
||||
'''
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
# Import Python Libs
|
||||
import logging
|
||||
import re
|
||||
|
||||
# Import Salt Libs
|
||||
import salt.exceptions
|
||||
from salt._compat import ElementTree as ET
|
||||
|
||||
# This must be present or the Salt loader won't load this module.
|
||||
__proxyenabled__ = ['cimc']
|
||||
|
||||
# Variables are scoped to this module so we can have persistent data.
|
||||
GRAINS_CACHE = {'vendor': 'Cisco'}
|
||||
DETAILS = {}
|
||||
|
||||
# Set up logging
|
||||
log = logging.getLogger(__file__)
|
||||
|
||||
# Define the module's virtual name
|
||||
__virtualname__ = 'cimc'
|
||||
|
||||
|
||||
def __virtual__():
|
||||
'''
|
||||
Only return if all the modules are available.
|
||||
'''
|
||||
return __virtualname__
|
||||
|
||||
|
||||
def init(opts):
|
||||
'''
|
||||
This function gets called when the proxy starts up.
|
||||
'''
|
||||
if 'host' not in opts['proxy']:
|
||||
log.critical('No \'host\' key found in pillar for this proxy.')
|
||||
return False
|
||||
if 'username' not in opts['proxy']:
|
||||
log.critical('No \'username\' key found in pillar for this proxy.')
|
||||
return False
|
||||
if 'password' not in opts['proxy']:
|
||||
log.critical('No \'passwords\' key found in pillar for this proxy.')
|
||||
return False
|
||||
|
||||
DETAILS['url'] = 'https://{0}/nuova'.format(opts['proxy']['host'])
|
||||
DETAILS['headers'] = {'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Length': 62,
|
||||
'USER-Agent': 'lwp-request/2.06'}
|
||||
|
||||
# Set configuration details
|
||||
DETAILS['host'] = opts['proxy']['host']
|
||||
DETAILS['username'] = opts['proxy'].get('username')
|
||||
DETAILS['password'] = opts['proxy'].get('password')
|
||||
|
||||
# Ensure connectivity to the device
|
||||
log.debug("Attempting to connect to cimc proxy host.")
|
||||
get_config_resolver_class("computeRackUnit")
|
||||
log.debug("Successfully connected to cimc proxy host.")
|
||||
|
||||
DETAILS['initialized'] = True
|
||||
|
||||
|
||||
def set_config_modify(dn=None, inconfig=None, hierarchical=False):
|
||||
'''
|
||||
The configConfMo method configures the specified managed object in a single subtree (for example, DN).
|
||||
'''
|
||||
ret = {}
|
||||
cookie = logon()
|
||||
|
||||
# Declare if the search contains hierarchical results.
|
||||
h = "false"
|
||||
if hierarchical is True:
|
||||
h = "true"
|
||||
|
||||
payload = '<configConfMo cookie="{0}" inHierarchical="{1}" dn="{2}">' \
|
||||
'<inConfig>{3}</inConfig></configConfMo>'.format(cookie, h, dn, inconfig)
|
||||
r = __utils__['http.query'](DETAILS['url'],
|
||||
data=payload,
|
||||
method='POST',
|
||||
decode_type='plain',
|
||||
decode=True,
|
||||
verify_ssl=False,
|
||||
raise_error=True,
|
||||
headers=DETAILS['headers'])
|
||||
answer = re.findall(r'(<[\s\S.]*>)', r['text'])[0]
|
||||
items = ET.fromstring(answer)
|
||||
logout(cookie)
|
||||
for item in items:
|
||||
ret[item.tag] = prepare_return(item)
|
||||
return ret
|
||||
|
||||
|
||||
def get_config_resolver_class(cid=None, hierarchical=False):
|
||||
'''
|
||||
The configResolveClass method returns requested managed object in a given class.
|
||||
'''
|
||||
ret = {}
|
||||
cookie = logon()
|
||||
|
||||
# Declare if the search contains hierarchical results.
|
||||
h = "false"
|
||||
if hierarchical is True:
|
||||
h = "true"
|
||||
|
||||
payload = '<configResolveClass cookie="{0}" inHierarchical="{1}" classId="{2}"/>'.format(cookie, h, cid)
|
||||
r = __utils__['http.query'](DETAILS['url'],
|
||||
data=payload,
|
||||
method='POST',
|
||||
decode_type='plain',
|
||||
decode=True,
|
||||
verify_ssl=False,
|
||||
raise_error=True,
|
||||
headers=DETAILS['headers'])
|
||||
|
||||
answer = re.findall(r'(<[\s\S.]*>)', r['text'])[0]
|
||||
items = ET.fromstring(answer)
|
||||
logout(cookie)
|
||||
for item in items:
|
||||
ret[item.tag] = prepare_return(item)
|
||||
return ret
|
||||
|
||||
|
||||
def logon():
|
||||
'''
|
||||
Logs into the cimc device and returns the session cookie.
|
||||
'''
|
||||
content = {}
|
||||
payload = "<aaaLogin inName='{0}' inPassword='{1}'></aaaLogin>".format(DETAILS['username'], DETAILS['password'])
|
||||
r = __utils__['http.query'](DETAILS['url'],
|
||||
data=payload,
|
||||
method='POST',
|
||||
decode_type='plain',
|
||||
decode=True,
|
||||
verify_ssl=False,
|
||||
raise_error=False,
|
||||
headers=DETAILS['headers'])
|
||||
answer = re.findall(r'(<[\s\S.]*>)', r['text'])[0]
|
||||
items = ET.fromstring(answer)
|
||||
for item in items.attrib:
|
||||
content[item] = items.attrib[item]
|
||||
|
||||
if 'outCookie' not in content:
|
||||
raise salt.exceptions.CommandExecutionError("Unable to log into proxy device.")
|
||||
|
||||
return content['outCookie']
|
||||
|
||||
|
||||
def logout(cookie=None):
|
||||
'''
|
||||
Closes the session with the device.
|
||||
'''
|
||||
payload = '<aaaLogout cookie="{0}" inCookie="{0}"></aaaLogout>'.format(cookie)
|
||||
__utils__['http.query'](DETAILS['url'],
|
||||
data=payload,
|
||||
method='POST',
|
||||
decode_type='plain',
|
||||
decode=True,
|
||||
verify_ssl=False,
|
||||
raise_error=True,
|
||||
headers=DETAILS['headers'])
|
||||
return
|
||||
|
||||
|
||||
def prepare_return(x):
|
||||
'''
|
||||
Converts the etree to dict
|
||||
'''
|
||||
ret = {}
|
||||
for a in list(x):
|
||||
if a.tag not in ret:
|
||||
ret[a.tag] = []
|
||||
ret[a.tag].append(prepare_return(a))
|
||||
for a in x.attrib:
|
||||
ret[a] = x.attrib[a]
|
||||
return ret
|
||||
|
||||
|
||||
def initialized():
|
||||
'''
|
||||
Since grains are loaded in many different places and some of those
|
||||
places occur before the proxy can be initialized, return whether
|
||||
our init() function has been called
|
||||
'''
|
||||
return DETAILS.get('initialized', False)
|
||||
|
||||
|
||||
def grains():
|
||||
'''
|
||||
Get the grains from the proxied device
|
||||
'''
|
||||
if not DETAILS.get('grains_cache', {}):
|
||||
DETAILS['grains_cache'] = GRAINS_CACHE
|
||||
try:
|
||||
compute_rack = get_config_resolver_class('computeRackUnit', False)
|
||||
DETAILS['grains_cache'] = compute_rack['outConfigs']['computeRackUnit']
|
||||
except Exception as err:
|
||||
log.error(err)
|
||||
return DETAILS['grains_cache']
|
||||
|
||||
|
||||
def grains_refresh():
|
||||
'''
|
||||
Refresh the grains from the proxied device
|
||||
'''
|
||||
DETAILS['grains_cache'] = None
|
||||
return grains()
|
||||
|
||||
|
||||
def ping():
|
||||
'''
|
||||
Returns true if the device is reachable, else false.
|
||||
'''
|
||||
try:
|
||||
cookie = logon()
|
||||
logout(cookie)
|
||||
except Exception as err:
|
||||
log.debug(err)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def shutdown():
|
||||
'''
|
||||
Shutdown the connection to the proxy device. For this proxy,
|
||||
shutdown is a no-op.
|
||||
'''
|
||||
log.debug('CIMC proxy shutdown() called.')
|
310
salt/proxy/esxcluster.py
Normal file
310
salt/proxy/esxcluster.py
Normal file
@ -0,0 +1,310 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
Proxy Minion interface module for managing VMWare ESXi clusters.
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
- pyVmomi
|
||||
- jsonschema
|
||||
|
||||
Configuration
|
||||
=============
|
||||
To use this integration proxy module, please configure the following:
|
||||
|
||||
Pillar
|
||||
------
|
||||
|
||||
Proxy minions get their configuration from Salt's Pillar. This can now happen
|
||||
from the proxy's configuration file.
|
||||
|
||||
Example pillars:
|
||||
|
||||
``userpass`` mechanism:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
proxy:
|
||||
proxytype: esxcluster
|
||||
cluster: <cluster name>
|
||||
datacenter: <datacenter name>
|
||||
vcenter: <ip or dns name of parent vcenter>
|
||||
mechanism: userpass
|
||||
username: <vCenter username>
|
||||
passwords: (required if userpass is used)
|
||||
- first_password
|
||||
- second_password
|
||||
- third_password
|
||||
|
||||
``sspi`` mechanism:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
proxy:
|
||||
proxytype: esxcluster
|
||||
cluster: <cluster name>
|
||||
datacenter: <datacenter name>
|
||||
vcenter: <ip or dns name of parent vcenter>
|
||||
mechanism: sspi
|
||||
domain: <user domain>
|
||||
principal: <host kerberos principal>
|
||||
|
||||
proxytype
|
||||
^^^^^^^^^
|
||||
To use this Proxy Module, set this to ``esxdatacenter``.
|
||||
|
||||
cluster
|
||||
^^^^^^^
|
||||
Name of the managed cluster. Required.
|
||||
|
||||
datacenter
|
||||
^^^^^^^^^^
|
||||
Name of the datacenter the managed cluster is in. Required.
|
||||
|
||||
vcenter
|
||||
^^^^^^^
|
||||
The location of the VMware vCenter server (host of ip) where the datacenter
|
||||
should be managed. Required.
|
||||
|
||||
mechanism
|
||||
^^^^^^^^
|
||||
The mechanism used to connect to the vCenter server. Supported values are
|
||||
``userpass`` and ``sspi``. Required.
|
||||
|
||||
Note:
|
||||
Connections are attempted using all (``username``, ``password``)
|
||||
combinations on proxy startup.
|
||||
|
||||
username
|
||||
^^^^^^^^
|
||||
The username used to login to the host, such as ``root``. Required if mechanism
|
||||
is ``userpass``.
|
||||
|
||||
passwords
|
||||
^^^^^^^^^
|
||||
A list of passwords to be used to try and login to the vCenter server. At least
|
||||
one password in this list is required if mechanism is ``userpass``. When the
|
||||
proxy comes up, it will try the passwords listed in order.
|
||||
|
||||
domain
|
||||
^^^^^^
|
||||
User domain. Required if mechanism is ``sspi``.
|
||||
|
||||
principal
|
||||
^^^^^^^^
|
||||
Kerberos principal. Rquired if mechanism is ``sspi``.
|
||||
|
||||
protocol
|
||||
^^^^^^^^
|
||||
If the ESXi host is not using the default protocol, set this value to an
|
||||
alternate protocol. Default is ``https``.
|
||||
|
||||
port
|
||||
^^^^
|
||||
If the ESXi host is not using the default port, set this value to an
|
||||
alternate port. Default is ``443``.
|
||||
|
||||
Salt Proxy
|
||||
----------
|
||||
|
||||
After your pillar is in place, you can test the proxy. The proxy can run on
|
||||
any machine that has network connectivity to your Salt Master and to the
|
||||
vCenter server in the pillar. SaltStack recommends that the machine running the
|
||||
salt-proxy process also run a regular minion, though it is not strictly
|
||||
necessary.
|
||||
|
||||
To start a proxy minion one needs to establish its identity <id>:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt-proxy --proxyid <proxy_id>
|
||||
|
||||
On the machine that will run the proxy, make sure there is a configuration file
|
||||
present. By default this is ``/etc/salt/proxy``. If in a different location, the
|
||||
``<configuration_folder>`` has to be specified when running the proxy:
|
||||
file with at least the following in it:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt-proxy --proxyid <proxy_id> -c <configuration_folder>
|
||||
|
||||
Commands
|
||||
--------
|
||||
|
||||
Once the proxy is running it will connect back to the specified master and
|
||||
individual commands can be runs against it:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Master - minion communication
|
||||
salt <cluster_name> test.ping
|
||||
|
||||
# Test vcenter connection
|
||||
salt <cluster_name> vsphere.test_vcenter_connection
|
||||
|
||||
States
|
||||
------
|
||||
|
||||
Associated states are documented in
|
||||
:mod:`salt.states.esxcluster </ref/states/all/salt.states.esxcluster>`.
|
||||
Look there to find an example structure for Pillar as well as an example
|
||||
``.sls`` file for configuring an ESX cluster from scratch.
|
||||
'''
|
||||
|
||||
|
||||
# Import Python Libs
|
||||
from __future__ import absolute_import
|
||||
import logging
|
||||
import os
|
||||
|
||||
# Import Salt Libs
|
||||
import salt.exceptions
|
||||
from salt.config.schemas.esxcluster import EsxclusterProxySchema
|
||||
from salt.utils.dictupdate import merge
|
||||
|
||||
# This must be present or the Salt loader won't load this module.
|
||||
__proxyenabled__ = ['esxcluster']
|
||||
|
||||
# External libraries
|
||||
try:
|
||||
import jsonschema
|
||||
HAS_JSONSCHEMA = True
|
||||
except ImportError:
|
||||
HAS_JSONSCHEMA = False
|
||||
|
||||
# Variables are scoped to this module so we can have persistent data
|
||||
# across calls to fns in here.
|
||||
GRAINS_CACHE = {}
|
||||
DETAILS = {}
|
||||
|
||||
|
||||
# Set up logging
|
||||
log = logging.getLogger(__name__)
|
||||
# Define the module's virtual name
|
||||
__virtualname__ = 'esxcluster'
|
||||
|
||||
|
||||
def __virtual__():
|
||||
'''
|
||||
Only load if the vsphere execution module is available.
|
||||
'''
|
||||
if HAS_JSONSCHEMA:
|
||||
return __virtualname__
|
||||
|
||||
return False, 'The esxcluster proxy module did not load.'
|
||||
|
||||
|
||||
def init(opts):
|
||||
'''
|
||||
This function gets called when the proxy starts up. For
|
||||
login
|
||||
the protocol and port are cached.
|
||||
'''
|
||||
log.debug('Initting esxcluster proxy module in process '
|
||||
'{}'.format(os.getpid()))
|
||||
log.debug('Validating esxcluster proxy input')
|
||||
schema = EsxclusterProxySchema.serialize()
|
||||
log.trace('schema = {}'.format(schema))
|
||||
proxy_conf = merge(opts.get('proxy', {}), __pillar__.get('proxy', {}))
|
||||
log.trace('proxy_conf = {0}'.format(proxy_conf))
|
||||
try:
|
||||
jsonschema.validate(proxy_conf, schema)
|
||||
except jsonschema.exceptions.ValidationError as exc:
|
||||
raise salt.exceptions.InvalidConfigError(exc)
|
||||
|
||||
# Save mandatory fields in cache
|
||||
for key in ('vcenter', 'datacenter', 'cluster', 'mechanism'):
|
||||
DETAILS[key] = proxy_conf[key]
|
||||
|
||||
# Additional validation
|
||||
if DETAILS['mechanism'] == 'userpass':
|
||||
if 'username' not in proxy_conf:
|
||||
raise salt.exceptions.InvalidConfigError(
|
||||
'Mechanism is set to \'userpass\', but no '
|
||||
'\'username\' key found in proxy config.')
|
||||
if 'passwords' not in proxy_conf:
|
||||
raise salt.exceptions.InvalidConfigError(
|
||||
'Mechanism is set to \'userpass\', but no '
|
||||
'\'passwords\' key found in proxy config.')
|
||||
for key in ('username', 'passwords'):
|
||||
DETAILS[key] = proxy_conf[key]
|
||||
else:
|
||||
if 'domain' not in proxy_conf:
|
||||
raise salt.exceptions.InvalidConfigError(
|
||||
'Mechanism is set to \'sspi\', but no '
|
||||
'\'domain\' key found in proxy config.')
|
||||
if 'principal' not in proxy_conf:
|
||||
raise salt.exceptions.InvalidConfigError(
|
||||
'Mechanism is set to \'sspi\', but no '
|
||||
'\'principal\' key found in proxy config.')
|
||||
for key in ('domain', 'principal'):
|
||||
DETAILS[key] = proxy_conf[key]
|
||||
|
||||
# Save optional
|
||||
DETAILS['protocol'] = proxy_conf.get('protocol')
|
||||
DETAILS['port'] = proxy_conf.get('port')
|
||||
|
||||
# Test connection
|
||||
if DETAILS['mechanism'] == 'userpass':
|
||||
# Get the correct login details
|
||||
log.debug('Retrieving credentials and testing vCenter connection for '
|
||||
'mehchanism \'userpass\'')
|
||||
try:
|
||||
username, password = find_credentials()
|
||||
DETAILS['password'] = password
|
||||
except salt.exceptions.SaltSystemExit as err:
|
||||
log.critical('Error: {0}'.format(err))
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def ping():
|
||||
'''
|
||||
Returns True.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt esx-cluster test.ping
|
||||
'''
|
||||
return True
|
||||
|
||||
|
||||
def shutdown():
|
||||
'''
|
||||
Shutdown the connection to the proxy device. For this proxy,
|
||||
shutdown is a no-op.
|
||||
'''
|
||||
log.debug('esxcluster proxy shutdown() called...')
|
||||
|
||||
|
||||
def find_credentials():
|
||||
'''
|
||||
Cycle through all the possible credentials and return the first one that
|
||||
works.
|
||||
'''
|
||||
|
||||
# if the username and password were already found don't fo though the
|
||||
# connection process again
|
||||
if 'username' in DETAILS and 'password' in DETAILS:
|
||||
return DETAILS['username'], DETAILS['password']
|
||||
|
||||
passwords = DETAILS['passwords']
|
||||
for password in passwords:
|
||||
DETAILS['password'] = password
|
||||
if not __salt__['vsphere.test_vcenter_connection']():
|
||||
# We are unable to authenticate
|
||||
continue
|
||||
# If we have data returned from above, we've successfully authenticated.
|
||||
return DETAILS['username'], password
|
||||
# We've reached the end of the list without successfully authenticating.
|
||||
raise salt.exceptions.VMwareConnectionError('Cannot complete login due to '
|
||||
'incorrect credentials.')
|
||||
|
||||
|
||||
def get_details():
|
||||
'''
|
||||
Function that returns the cached details
|
||||
'''
|
||||
return DETAILS
|
@ -116,9 +116,14 @@ def cert(name,
|
||||
if res['result'] is None:
|
||||
ret['changes'] = {}
|
||||
else:
|
||||
if not __salt__['acme.has'](name):
|
||||
new = None
|
||||
else:
|
||||
new = __salt__['acme.info'](name)
|
||||
|
||||
ret['changes'] = {
|
||||
'old': old,
|
||||
'new': __salt__['acme.info'](name)
|
||||
'new': new
|
||||
}
|
||||
|
||||
return ret
|
||||
|
211
salt/states/cimc.py
Normal file
211
salt/states/cimc.py
Normal file
@ -0,0 +1,211 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
A state module to manage Cisco UCS chassis devices.
|
||||
|
||||
:codeauthor: :email:`Spencer Ervin <spencer_ervin@hotmail.com>`
|
||||
:maturity: new
|
||||
:depends: none
|
||||
:platform: unix
|
||||
|
||||
|
||||
About
|
||||
=====
|
||||
This state module was designed to handle connections to a Cisco Unified Computing System (UCS) chassis. This module
|
||||
relies on the CIMC proxy module to interface with the device.
|
||||
|
||||
.. seealso::
|
||||
:prox:`CIMC Proxy Module <salt.proxy.cimc>`
|
||||
|
||||
'''
|
||||
|
||||
# Import Python Libs
|
||||
from __future__ import absolute_import
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def __virtual__():
|
||||
return 'cimc.get_system_info' in __salt__
|
||||
|
||||
|
||||
def _default_ret(name):
|
||||
'''
|
||||
Set the default response values.
|
||||
|
||||
'''
|
||||
ret = {
|
||||
'name': name,
|
||||
'changes': {},
|
||||
'result': False,
|
||||
'comment': ''
|
||||
}
|
||||
return ret
|
||||
|
||||
|
||||
def ntp(name, servers):
|
||||
'''
|
||||
Ensures that the NTP servers are configured. Servers are provided as an individual string or list format. Only four
|
||||
NTP servers will be reviewed. Any entries past four will be ignored.
|
||||
|
||||
name: The name of the module function to execute.
|
||||
|
||||
servers(str, list): The IP address or FQDN of the NTP servers.
|
||||
|
||||
SLS Example:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
ntp_configuration_list:
|
||||
cimc.ntp:
|
||||
- servers:
|
||||
- foo.bar.com
|
||||
- 10.10.10.10
|
||||
|
||||
ntp_configuration_str:
|
||||
cimc.ntp:
|
||||
- servers: foo.bar.com
|
||||
|
||||
'''
|
||||
ret = _default_ret(name)
|
||||
|
||||
ntp_servers = ['', '', '', '']
|
||||
|
||||
# Parse our server arguments
|
||||
if isinstance(servers, list):
|
||||
i = 0
|
||||
for x in servers:
|
||||
ntp_servers[i] = x
|
||||
i += 1
|
||||
else:
|
||||
ntp_servers[0] = servers
|
||||
|
||||
conf = __salt__['cimc.get_ntp']()
|
||||
|
||||
# Check if our NTP configuration is already set
|
||||
req_change = False
|
||||
try:
|
||||
if conf['outConfigs']['commNtpProvider'][0]['ntpEnable'] != 'yes' \
|
||||
or ntp_servers[0] != conf['outConfigs']['commNtpProvider'][0]['ntpServer1'] \
|
||||
or ntp_servers[1] != conf['outConfigs']['commNtpProvider'][0]['ntpServer2'] \
|
||||
or ntp_servers[2] != conf['outConfigs']['commNtpProvider'][0]['ntpServer3'] \
|
||||
or ntp_servers[3] != conf['outConfigs']['commNtpProvider'][0]['ntpServer4']:
|
||||
req_change = True
|
||||
except KeyError as err:
|
||||
ret['result'] = False
|
||||
ret['comment'] = "Unable to confirm current NTP settings."
|
||||
log.error(err)
|
||||
return ret
|
||||
|
||||
if req_change:
|
||||
|
||||
try:
|
||||
update = __salt__['cimc.set_ntp_server'](ntp_servers[0],
|
||||
ntp_servers[1],
|
||||
ntp_servers[2],
|
||||
ntp_servers[3])
|
||||
if update['outConfig']['commNtpProvider'][0]['status'] != 'modified':
|
||||
ret['result'] = False
|
||||
ret['comment'] = "Error setting NTP configuration."
|
||||
return ret
|
||||
except Exception as err:
|
||||
ret['result'] = False
|
||||
ret['comment'] = "Error setting NTP configuration."
|
||||
log.error(err)
|
||||
return ret
|
||||
|
||||
ret['changes']['before'] = conf
|
||||
ret['changes']['after'] = __salt__['cimc.get_ntp']()
|
||||
ret['comment'] = "NTP settings modified."
|
||||
else:
|
||||
ret['comment'] = "NTP already configured. No changes required."
|
||||
|
||||
ret['result'] = True
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def syslog(name, primary=None, secondary=None):
|
||||
'''
|
||||
Ensures that the syslog servers are set to the specified values. A value of None will be ignored.
|
||||
|
||||
name: The name of the module function to execute.
|
||||
|
||||
primary(str): The IP address or FQDN of the primary syslog server.
|
||||
|
||||
secondary(str): The IP address or FQDN of the secondary syslog server.
|
||||
|
||||
SLS Example:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
syslog_configuration:
|
||||
cimc.syslog:
|
||||
- primary: 10.10.10.10
|
||||
- secondary: foo.bar.com
|
||||
|
||||
'''
|
||||
ret = _default_ret(name)
|
||||
|
||||
conf = __salt__['cimc.get_syslog']()
|
||||
|
||||
req_change = False
|
||||
|
||||
if primary:
|
||||
prim_change = True
|
||||
if 'outConfigs' in conf and 'commSyslogClient' in conf['outConfigs']:
|
||||
for entry in conf['outConfigs']['commSyslogClient']:
|
||||
if entry['name'] != 'primary':
|
||||
continue
|
||||
if entry['adminState'] == 'enabled' and entry['hostname'] == primary:
|
||||
prim_change = False
|
||||
|
||||
if prim_change:
|
||||
try:
|
||||
update = __salt__['cimc.set_syslog_server'](primary, "primary")
|
||||
if update['outConfig']['commSyslogClient'][0]['status'] == 'modified':
|
||||
req_change = True
|
||||
else:
|
||||
ret['result'] = False
|
||||
ret['comment'] = "Error setting primary SYSLOG server."
|
||||
return ret
|
||||
except Exception as err:
|
||||
ret['result'] = False
|
||||
ret['comment'] = "Error setting primary SYSLOG server."
|
||||
log.error(err)
|
||||
return ret
|
||||
|
||||
if secondary:
|
||||
sec_change = True
|
||||
if 'outConfig' in conf and 'commSyslogClient' in conf['outConfig']:
|
||||
for entry in conf['outConfig']['commSyslogClient']:
|
||||
if entry['name'] != 'secondary':
|
||||
continue
|
||||
if entry['adminState'] == 'enabled' and entry['hostname'] == secondary:
|
||||
sec_change = False
|
||||
|
||||
if sec_change:
|
||||
try:
|
||||
update = __salt__['cimc.set_syslog_server'](secondary, "secondary")
|
||||
if update['outConfig']['commSyslogClient'][0]['status'] == 'modified':
|
||||
req_change = True
|
||||
else:
|
||||
ret['result'] = False
|
||||
ret['comment'] = "Error setting secondary SYSLOG server."
|
||||
return ret
|
||||
except Exception as err:
|
||||
ret['result'] = False
|
||||
ret['comment'] = "Error setting secondary SYSLOG server."
|
||||
log.error(err)
|
||||
return ret
|
||||
|
||||
if req_change:
|
||||
ret['changes']['before'] = conf
|
||||
ret['changes']['after'] = __salt__['cimc.get_syslog']()
|
||||
ret['comment'] = "SYSLOG settings modified."
|
||||
else:
|
||||
ret['comment'] = "SYSLOG already configured. No changes required."
|
||||
|
||||
ret['result'] = True
|
||||
|
||||
return ret
|
269
salt/utils/configparser.py
Normal file
269
salt/utils/configparser.py
Normal file
@ -0,0 +1,269 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Import Python libs
|
||||
from __future__ import absolute_import
|
||||
import re
|
||||
|
||||
# Import Salt libs
|
||||
import salt.utils.stringutils
|
||||
|
||||
# Import 3rd-party libs
|
||||
from salt.ext import six
|
||||
from salt.ext.six.moves.configparser import * # pylint: disable=no-name-in-module,wildcard-import
|
||||
|
||||
try:
|
||||
from collections import OrderedDict as _default_dict
|
||||
except ImportError:
|
||||
# fallback for setup.py which hasn't yet built _collections
|
||||
_default_dict = dict
|
||||
|
||||
|
||||
# pylint: disable=string-substitution-usage-error
|
||||
class GitConfigParser(RawConfigParser, object): # pylint: disable=undefined-variable
|
||||
'''
|
||||
Custom ConfigParser which reads and writes git config files.
|
||||
|
||||
READ A GIT CONFIG FILE INTO THE PARSER OBJECT
|
||||
|
||||
>>> import salt.utils.configparser
|
||||
>>> conf = salt.utils.configparser.GitConfigParser()
|
||||
>>> conf.read('/home/user/.git/config')
|
||||
|
||||
MAKE SOME CHANGES
|
||||
|
||||
>>> # Change user.email
|
||||
>>> conf.set('user', 'email', 'myaddress@mydomain.tld')
|
||||
>>> # Add another refspec to the "origin" remote's "fetch" multivar
|
||||
>>> conf.set_multivar('remote "origin"', 'fetch', '+refs/tags/*:refs/tags/*')
|
||||
|
||||
WRITE THE CONFIG TO A FILEHANDLE
|
||||
|
||||
>>> import salt.utils.files
|
||||
>>> with salt.utils.files.fopen('/home/user/.git/config', 'w') as fh:
|
||||
... conf.write(fh)
|
||||
>>>
|
||||
'''
|
||||
DEFAULTSECT = u'DEFAULT'
|
||||
SPACEINDENT = u' ' * 8
|
||||
|
||||
def __init__(self, defaults=None, dict_type=_default_dict,
|
||||
allow_no_value=True):
|
||||
'''
|
||||
Changes default value for allow_no_value from False to True
|
||||
'''
|
||||
super(GitConfigParser, self).__init__(
|
||||
defaults, dict_type, allow_no_value)
|
||||
|
||||
def _read(self, fp, fpname):
|
||||
'''
|
||||
Makes the following changes from the RawConfigParser:
|
||||
|
||||
1. Strip leading tabs from non-section-header lines.
|
||||
2. Treat 8 spaces at the beginning of a line as a tab.
|
||||
3. Treat lines beginning with a tab as options.
|
||||
4. Drops support for continuation lines.
|
||||
5. Multiple values for a given option are stored as a list.
|
||||
6. Keys and values are decoded to the system encoding.
|
||||
'''
|
||||
cursect = None # None, or a dictionary
|
||||
optname = None
|
||||
lineno = 0
|
||||
e = None # None, or an exception
|
||||
while True:
|
||||
line = fp.readline()
|
||||
if six.PY2:
|
||||
line = line.decode(__salt_system_encoding__)
|
||||
if not line:
|
||||
break
|
||||
lineno = lineno + 1
|
||||
# comment or blank line?
|
||||
if line.strip() == u'' or line[0] in u'#;':
|
||||
continue
|
||||
if line.split(None, 1)[0].lower() == u'rem' and line[0] in u'rR':
|
||||
# no leading whitespace
|
||||
continue
|
||||
# Replace space indentation with a tab. Allows parser to work
|
||||
# properly in cases where someone has edited the git config by hand
|
||||
# and indented using spaces instead of tabs.
|
||||
if line.startswith(self.SPACEINDENT):
|
||||
line = u'\t' + line[len(self.SPACEINDENT):]
|
||||
# is it a section header?
|
||||
mo = self.SECTCRE.match(line)
|
||||
if mo:
|
||||
sectname = mo.group(u'header')
|
||||
if sectname in self._sections:
|
||||
cursect = self._sections[sectname]
|
||||
elif sectname == self.DEFAULTSECT:
|
||||
cursect = self._defaults
|
||||
else:
|
||||
cursect = self._dict()
|
||||
self._sections[sectname] = cursect
|
||||
# So sections can't start with a continuation line
|
||||
optname = None
|
||||
# no section header in the file?
|
||||
elif cursect is None:
|
||||
raise MissingSectionHeaderError( # pylint: disable=undefined-variable
|
||||
salt.utils.stringutils.to_str(fpname),
|
||||
lineno,
|
||||
salt.utils.stringutils.to_str(line))
|
||||
# an option line?
|
||||
else:
|
||||
mo = self._optcre.match(line.lstrip())
|
||||
if mo:
|
||||
optname, vi, optval = mo.group(u'option', u'vi', u'value')
|
||||
optname = self.optionxform(optname.rstrip())
|
||||
if optval is None:
|
||||
optval = u''
|
||||
if optval:
|
||||
if vi in (u'=', u':') and u';' in optval:
|
||||
# ';' is a comment delimiter only if it follows
|
||||
# a spacing character
|
||||
pos = optval.find(u';')
|
||||
if pos != -1 and optval[pos-1].isspace():
|
||||
optval = optval[:pos]
|
||||
optval = optval.strip()
|
||||
# Empty strings should be considered as blank strings
|
||||
if optval in (u'""', u"''"):
|
||||
optval = u''
|
||||
self._add_option(cursect, optname, optval)
|
||||
else:
|
||||
# a non-fatal parsing error occurred. set up the
|
||||
# exception but keep going. the exception will be
|
||||
# raised at the end of the file and will contain a
|
||||
# list of all bogus lines
|
||||
if not e:
|
||||
e = ParsingError(fpname) # pylint: disable=undefined-variable
|
||||
e.append(lineno, repr(line))
|
||||
# if any parsing errors occurred, raise an exception
|
||||
if e:
|
||||
raise e # pylint: disable=raising-bad-type
|
||||
|
||||
def _string_check(self, value, allow_list=False):
|
||||
'''
|
||||
Based on the string-checking code from the SafeConfigParser's set()
|
||||
function, this enforces string values for config options.
|
||||
'''
|
||||
if self._optcre is self.OPTCRE or value:
|
||||
is_list = isinstance(value, list)
|
||||
if is_list and not allow_list:
|
||||
raise TypeError('option value cannot be a list unless allow_list is True') # future lint: disable=non-unicode-string
|
||||
elif not is_list:
|
||||
value = [value]
|
||||
if not all(isinstance(x, six.string_types) for x in value):
|
||||
raise TypeError('option values must be strings') # future lint: disable=non-unicode-string
|
||||
|
||||
def get(self, section, option, as_list=False):
|
||||
'''
|
||||
Adds an optional "as_list" argument to ensure a list is returned. This
|
||||
is helpful when iterating over an option which may or may not be a
|
||||
multivar.
|
||||
'''
|
||||
ret = super(GitConfigParser, self).get(section, option)
|
||||
if as_list and not isinstance(ret, list):
|
||||
ret = [ret]
|
||||
return ret
|
||||
|
||||
def set(self, section, option, value=u''):
|
||||
'''
|
||||
This is overridden from the RawConfigParser merely to change the
|
||||
default value for the 'value' argument.
|
||||
'''
|
||||
self._string_check(value)
|
||||
super(GitConfigParser, self).set(section, option, value)
|
||||
|
||||
def _add_option(self, sectdict, key, value):
|
||||
if isinstance(value, list):
|
||||
sectdict[key] = value
|
||||
elif isinstance(value, six.string_types):
|
||||
try:
|
||||
sectdict[key].append(value)
|
||||
except KeyError:
|
||||
# Key not present, set it
|
||||
sectdict[key] = value
|
||||
except AttributeError:
|
||||
# Key is present but the value is not a list. Make it into a list
|
||||
# and then append to it.
|
||||
sectdict[key] = [sectdict[key]]
|
||||
sectdict[key].append(value)
|
||||
else:
|
||||
raise TypeError('Expected str or list for option value, got %s' % type(value).__name__) # future lint: disable=non-unicode-string
|
||||
|
||||
def set_multivar(self, section, option, value=u''):
|
||||
'''
|
||||
This function is unique to the GitConfigParser. It will add another
|
||||
value for the option if it already exists, converting the option's
|
||||
value to a list if applicable.
|
||||
|
||||
If "value" is a list, then any existing values for the specified
|
||||
section and option will be replaced with the list being passed.
|
||||
'''
|
||||
self._string_check(value, allow_list=True)
|
||||
if not section or section == self.DEFAULTSECT:
|
||||
sectdict = self._defaults
|
||||
else:
|
||||
try:
|
||||
sectdict = self._sections[section]
|
||||
except KeyError:
|
||||
raise NoSectionError( # pylint: disable=undefined-variable
|
||||
salt.utils.stringutils.to_str(section))
|
||||
key = self.optionxform(option)
|
||||
self._add_option(sectdict, key, value)
|
||||
|
||||
def remove_option_regexp(self, section, option, expr):
|
||||
'''
|
||||
Remove an option with a value matching the expression. Works on single
|
||||
values and multivars.
|
||||
'''
|
||||
if not section or section == self.DEFAULTSECT:
|
||||
sectdict = self._defaults
|
||||
else:
|
||||
try:
|
||||
sectdict = self._sections[section]
|
||||
except KeyError:
|
||||
raise NoSectionError( # pylint: disable=undefined-variable
|
||||
salt.utils.stringutils.to_str(section))
|
||||
option = self.optionxform(option)
|
||||
if option not in sectdict:
|
||||
return False
|
||||
regexp = re.compile(expr)
|
||||
if isinstance(sectdict[option], list):
|
||||
new_list = [x for x in sectdict[option] if not regexp.search(x)]
|
||||
# Revert back to a list if we removed all but one item
|
||||
if len(new_list) == 1:
|
||||
new_list = new_list[0]
|
||||
existed = new_list != sectdict[option]
|
||||
if existed:
|
||||
del sectdict[option]
|
||||
sectdict[option] = new_list
|
||||
del new_list
|
||||
else:
|
||||
existed = bool(regexp.search(sectdict[option]))
|
||||
if existed:
|
||||
del sectdict[option]
|
||||
return existed
|
||||
|
||||
def write(self, fp_):
|
||||
'''
|
||||
Makes the following changes from the RawConfigParser:
|
||||
|
||||
1. Prepends options with a tab character.
|
||||
2. Does not write a blank line between sections.
|
||||
3. When an option's value is a list, a line for each option is written.
|
||||
This allows us to support multivars like a remote's "fetch" option.
|
||||
4. Drops support for continuation lines.
|
||||
'''
|
||||
convert = salt.utils.stringutils.to_bytes \
|
||||
if u'b' in fp_.mode \
|
||||
else salt.utils.stringutils.to_str
|
||||
if self._defaults:
|
||||
fp_.write(convert(u'[%s]\n' % self.DEFAULTSECT))
|
||||
for (key, value) in six.iteritems(self._defaults):
|
||||
value = salt.utils.stringutils.to_unicode(value).replace(u'\n', u'\n\t')
|
||||
fp_.write(convert(u'%s = %s\n' % (key, value)))
|
||||
for section in self._sections:
|
||||
fp_.write(convert(u'[%s]\n' % section))
|
||||
for (key, value) in six.iteritems(self._sections[section]):
|
||||
if (value is not None) or (self._optcre == self.OPTCRE):
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
for item in value:
|
||||
fp_.write(convert(u'\t%s\n' % u' = '.join((key, item)).rstrip()))
|
@ -21,6 +21,7 @@ from datetime import datetime
|
||||
|
||||
# Import salt libs
|
||||
import salt.utils
|
||||
import salt.utils.configparser
|
||||
import salt.utils.files
|
||||
import salt.utils.itertools
|
||||
import salt.utils.path
|
||||
@ -29,13 +30,12 @@ import salt.utils.stringutils
|
||||
import salt.utils.url
|
||||
import salt.utils.versions
|
||||
import salt.fileserver
|
||||
from salt.config import DEFAULT_MASTER_OPTS as __DEFAULT_MASTER_OPTS
|
||||
from salt.config import DEFAULT_MASTER_OPTS as _DEFAULT_MASTER_OPTS
|
||||
from salt.utils.odict import OrderedDict
|
||||
from salt.utils.process import os_is_running as pid_exists
|
||||
from salt.exceptions import (
|
||||
FileserverConfigError,
|
||||
GitLockError,
|
||||
GitRemoteError,
|
||||
get_error_message
|
||||
)
|
||||
from salt.utils.event import tagify
|
||||
@ -44,7 +44,7 @@ from salt.utils.versions import LooseVersion as _LooseVersion
|
||||
# Import third party libs
|
||||
from salt.ext import six
|
||||
|
||||
VALID_REF_TYPES = __DEFAULT_MASTER_OPTS['gitfs_ref_types']
|
||||
VALID_REF_TYPES = _DEFAULT_MASTER_OPTS['gitfs_ref_types']
|
||||
|
||||
# Optional per-remote params that can only be used on a per-remote basis, and
|
||||
# thus do not have defaults in salt/config.py.
|
||||
@ -327,6 +327,28 @@ class GitProvider(object):
|
||||
setattr(self, '_' + key, self.conf[key])
|
||||
self.add_conf_overlay(key)
|
||||
|
||||
if not hasattr(self, 'refspecs'):
|
||||
# This was not specified as a per-remote overrideable parameter
|
||||
# when instantiating an instance of a GitBase subclass. Make sure
|
||||
# that we set this attribute so we at least have a sane default and
|
||||
# are able to fetch.
|
||||
key = '{0}_refspecs'.format(self.role)
|
||||
try:
|
||||
default_refspecs = _DEFAULT_MASTER_OPTS[key]
|
||||
except KeyError:
|
||||
log.critical(
|
||||
'The \'%s\' option has no default value in '
|
||||
'salt/config/__init__.py.', key
|
||||
)
|
||||
failhard(self.role)
|
||||
|
||||
setattr(self, 'refspecs', default_refspecs)
|
||||
log.debug(
|
||||
'The \'refspecs\' option was not explicitly defined as a '
|
||||
'configurable parameter. Falling back to %s for %s remote '
|
||||
'\'%s\'.', default_refspecs, self.role, self.id
|
||||
)
|
||||
|
||||
for item in ('env_whitelist', 'env_blacklist'):
|
||||
val = getattr(self, item, None)
|
||||
if val:
|
||||
@ -493,12 +515,6 @@ class GitProvider(object):
|
||||
return strip_sep(getattr(self, '_' + name))
|
||||
setattr(cls, name, _getconf)
|
||||
|
||||
def add_refspecs(self, *refspecs):
|
||||
'''
|
||||
This function must be overridden in a sub-class
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def check_root(self):
|
||||
'''
|
||||
Check if the relative root path exists in the checked-out copy of the
|
||||
@ -591,55 +607,74 @@ class GitProvider(object):
|
||||
success.append(msg)
|
||||
return success, failed
|
||||
|
||||
def configure_refspecs(self):
|
||||
def enforce_git_config(self):
|
||||
'''
|
||||
Ensure that the configured refspecs are set
|
||||
For the config options which need to be maintained in the git config,
|
||||
ensure that the git config file is configured as desired.
|
||||
'''
|
||||
try:
|
||||
refspecs = set(self.get_refspecs())
|
||||
except (git.exc.GitCommandError, GitRemoteError) as exc:
|
||||
log.error(
|
||||
'Failed to get refspecs for %s remote \'%s\': %s',
|
||||
self.role,
|
||||
self.id,
|
||||
exc
|
||||
)
|
||||
return
|
||||
|
||||
desired_refspecs = set(self.refspecs)
|
||||
to_delete = refspecs - desired_refspecs if refspecs else set()
|
||||
if to_delete:
|
||||
# There is no native unset support in Pygit2, and GitPython just
|
||||
# wraps the CLI anyway. So we'll just use the git CLI to
|
||||
# --unset-all the config value. Then, we will add back all
|
||||
# configured refspecs. This is more foolproof than trying to remove
|
||||
# specific refspecs, as removing specific ones necessitates
|
||||
# formulating a regex to match, and the fact that slashes and
|
||||
# asterisks are in refspecs complicates this.
|
||||
cmd_str = 'git config --unset-all remote.origin.fetch'
|
||||
cmd = subprocess.Popen(
|
||||
shlex.split(cmd_str),
|
||||
close_fds=not salt.utils.platform.is_windows(),
|
||||
cwd=os.path.dirname(self.gitdir),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT)
|
||||
output = cmd.communicate()[0]
|
||||
if cmd.returncode != 0:
|
||||
log.error(
|
||||
'Failed to unset git config value for %s remote \'%s\'. '
|
||||
'Output from \'%s\' follows:\n%s',
|
||||
self.role, self.id, cmd_str, output
|
||||
)
|
||||
return
|
||||
# Since we had to remove all refspecs, we now need to add all
|
||||
# desired refspecs to achieve the desired configuration.
|
||||
to_add = desired_refspecs
|
||||
git_config = os.path.join(self.gitdir, 'config')
|
||||
conf = salt.utils.configparser.GitConfigParser()
|
||||
if not conf.read(git_config):
|
||||
log.error('Failed to read from git config file %s', git_config)
|
||||
else:
|
||||
# We didn't need to delete any refspecs, so we'll only need to add
|
||||
# the desired refspecs that aren't currently configured.
|
||||
to_add = desired_refspecs - refspecs
|
||||
# We are currently enforcing the following git config items:
|
||||
# 1. refspecs used in fetch
|
||||
# 2. http.sslVerify
|
||||
conf_changed = False
|
||||
|
||||
self.add_refspecs(*to_add)
|
||||
# 1. refspecs
|
||||
try:
|
||||
refspecs = sorted(
|
||||
conf.get('remote "origin"', 'fetch', as_list=True))
|
||||
except salt.utils.configparser.NoSectionError:
|
||||
# First time we've init'ed this repo, we need to add the
|
||||
# section for the remote to the git config
|
||||
conf.add_section('remote "origin"')
|
||||
conf.set('remote "origin"', 'url', self.url)
|
||||
conf_changed = True
|
||||
refspecs = []
|
||||
desired_refspecs = sorted(self.refspecs)
|
||||
log.debug(
|
||||
'Current refspecs for %s remote \'%s\': %s (desired: %s)',
|
||||
self.role, self.id, refspecs, desired_refspecs
|
||||
)
|
||||
if refspecs != desired_refspecs:
|
||||
conf.set_multivar('remote "origin"', 'fetch', self.refspecs)
|
||||
log.debug(
|
||||
'Refspecs for %s remote \'%s\' set to %s',
|
||||
self.role, self.id, desired_refspecs
|
||||
)
|
||||
conf_changed = True
|
||||
|
||||
# 2. http.sslVerify
|
||||
try:
|
||||
ssl_verify = conf.get('http', 'sslVerify')
|
||||
except salt.utils.configparser.NoSectionError:
|
||||
conf.add_section('http')
|
||||
ssl_verify = None
|
||||
except salt.utils.configparser.NoOptionError:
|
||||
ssl_verify = None
|
||||
desired_ssl_verify = six.text_type(self.ssl_verify).lower()
|
||||
log.debug(
|
||||
'Current http.sslVerify for %s remote \'%s\': %s (desired: %s)',
|
||||
self.role, self.id, ssl_verify, desired_ssl_verify
|
||||
)
|
||||
if ssl_verify != desired_ssl_verify:
|
||||
conf.set('http', 'sslVerify', desired_ssl_verify)
|
||||
log.debug(
|
||||
'http.sslVerify for %s remote \'%s\' set to %s',
|
||||
self.role, self.id, desired_ssl_verify
|
||||
)
|
||||
conf_changed = True
|
||||
|
||||
# Write changes, if necessary
|
||||
if conf_changed:
|
||||
with salt.utils.files.fopen(git_config, 'w') as fp_:
|
||||
conf.write(fp_)
|
||||
log.debug(
|
||||
'Config updates for %s remote \'%s\' written to %s',
|
||||
self.role, self.id, git_config
|
||||
)
|
||||
|
||||
def fetch(self):
|
||||
'''
|
||||
@ -853,12 +888,6 @@ class GitProvider(object):
|
||||
else target
|
||||
return self.branch
|
||||
|
||||
def get_refspecs(self):
|
||||
'''
|
||||
This function must be overridden in a sub-class
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_tree(self, tgt_env):
|
||||
'''
|
||||
Return a tree object for the specified environment
|
||||
@ -935,23 +964,6 @@ class GitPython(GitProvider):
|
||||
override_params, cache_root, role
|
||||
)
|
||||
|
||||
def add_refspecs(self, *refspecs):
|
||||
'''
|
||||
Add the specified refspecs to the "origin" remote
|
||||
'''
|
||||
for refspec in refspecs:
|
||||
try:
|
||||
self.repo.git.config('--add', 'remote.origin.fetch', refspec)
|
||||
log.debug(
|
||||
'Added refspec \'%s\' to %s remote \'%s\'',
|
||||
refspec, self.role, self.id
|
||||
)
|
||||
except git.exc.GitCommandError as exc:
|
||||
log.error(
|
||||
'Failed to add refspec \'%s\' to %s remote \'%s\': %s',
|
||||
refspec, self.role, self.id, exc
|
||||
)
|
||||
|
||||
def checkout(self):
|
||||
'''
|
||||
Checkout the configured branch/tag. We catch an "Exception" class here
|
||||
@ -1039,29 +1051,7 @@ class GitPython(GitProvider):
|
||||
return new
|
||||
|
||||
self.gitdir = salt.utils.path.join(self.repo.working_dir, '.git')
|
||||
|
||||
if not self.repo.remotes:
|
||||
try:
|
||||
self.repo.create_remote('origin', self.url)
|
||||
except os.error:
|
||||
# This exception occurs when two processes are trying to write
|
||||
# to the git config at once, go ahead and pass over it since
|
||||
# this is the only write. This should place a lock down.
|
||||
pass
|
||||
else:
|
||||
new = True
|
||||
|
||||
try:
|
||||
ssl_verify = self.repo.git.config('--get', 'http.sslVerify')
|
||||
except git.exc.GitCommandError:
|
||||
ssl_verify = ''
|
||||
desired_ssl_verify = str(self.ssl_verify).lower()
|
||||
if ssl_verify != desired_ssl_verify:
|
||||
self.repo.git.config('http.sslVerify', desired_ssl_verify)
|
||||
|
||||
# Ensure that refspecs for the "origin" remote are set up as configured
|
||||
if hasattr(self, 'refspecs'):
|
||||
self.configure_refspecs()
|
||||
self.enforce_git_config()
|
||||
|
||||
return new
|
||||
|
||||
@ -1213,13 +1203,6 @@ class GitPython(GitProvider):
|
||||
return blob, blob.hexsha, blob.mode
|
||||
return None, None, None
|
||||
|
||||
def get_refspecs(self):
|
||||
'''
|
||||
Return the configured refspecs
|
||||
'''
|
||||
refspecs = self.repo.git.config('--get-all', 'remote.origin.fetch')
|
||||
return [x.strip() for x in refspecs.splitlines()]
|
||||
|
||||
def get_tree_from_branch(self, ref):
|
||||
'''
|
||||
Return a git.Tree object matching a head ref fetched into
|
||||
@ -1272,27 +1255,6 @@ class Pygit2(GitProvider):
|
||||
override_params, cache_root, role
|
||||
)
|
||||
|
||||
def add_refspecs(self, *refspecs):
|
||||
'''
|
||||
Add the specified refspecs to the "origin" remote
|
||||
'''
|
||||
for refspec in refspecs:
|
||||
try:
|
||||
self.repo.config.set_multivar(
|
||||
'remote.origin.fetch',
|
||||
'FOO',
|
||||
refspec
|
||||
)
|
||||
log.debug(
|
||||
'Added refspec \'%s\' to %s remote \'%s\'',
|
||||
refspec, self.role, self.id
|
||||
)
|
||||
except Exception as exc:
|
||||
log.error(
|
||||
'Failed to add refspec \'%s\' to %s remote \'%s\': %s',
|
||||
refspec, self.role, self.id, exc
|
||||
)
|
||||
|
||||
def checkout(self):
|
||||
'''
|
||||
Checkout the configured branch/tag
|
||||
@ -1519,30 +1481,7 @@ class Pygit2(GitProvider):
|
||||
return new
|
||||
|
||||
self.gitdir = salt.utils.path.join(self.repo.workdir, '.git')
|
||||
|
||||
if not self.repo.remotes:
|
||||
try:
|
||||
self.repo.create_remote('origin', self.url)
|
||||
except os.error:
|
||||
# This exception occurs when two processes are trying to write
|
||||
# to the git config at once, go ahead and pass over it since
|
||||
# this is the only write. This should place a lock down.
|
||||
pass
|
||||
else:
|
||||
new = True
|
||||
|
||||
try:
|
||||
ssl_verify = self.repo.config.get_bool('http.sslVerify')
|
||||
except KeyError:
|
||||
ssl_verify = None
|
||||
if ssl_verify != self.ssl_verify:
|
||||
self.repo.config.set_multivar('http.sslVerify',
|
||||
'',
|
||||
str(self.ssl_verify).lower())
|
||||
|
||||
# Ensure that refspecs for the "origin" remote are set up as configured
|
||||
if hasattr(self, 'refspecs'):
|
||||
self.configure_refspecs()
|
||||
self.enforce_git_config()
|
||||
|
||||
return new
|
||||
|
||||
@ -1760,14 +1699,6 @@ class Pygit2(GitProvider):
|
||||
return blob, blob.hex, mode
|
||||
return None, None, None
|
||||
|
||||
def get_refspecs(self):
|
||||
'''
|
||||
Return the configured refspecs
|
||||
'''
|
||||
if not [x for x in self.repo.config if x.startswith('remote.origin.')]:
|
||||
raise GitRemoteError('\'origin\' remote not not present')
|
||||
return list(self.repo.config.get_multivar('remote.origin.fetch'))
|
||||
|
||||
def get_tree_from_branch(self, ref):
|
||||
'''
|
||||
Return a pygit2.Tree object matching a head ref fetched into
|
||||
|
38
tests/unit/modules/test_esxcluster.py
Normal file
38
tests/unit/modules/test_esxcluster.py
Normal file
@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
:codeauthor: :email:`Alexandru Bleotu <alexandru.bleotu@morganstanley.com>`
|
||||
|
||||
Tests for functions in salt.modules.esxcluster
|
||||
'''
|
||||
|
||||
# Import Python Libs
|
||||
from __future__ import absolute_import
|
||||
|
||||
# Import Salt Libs
|
||||
import salt.modules.esxcluster as esxcluster
|
||||
|
||||
# Import Salt Testing Libs
|
||||
from tests.support.mixins import LoaderModuleMockMixin
|
||||
from tests.support.unit import TestCase, skipIf
|
||||
from tests.support.mock import (
|
||||
MagicMock,
|
||||
patch,
|
||||
NO_MOCK,
|
||||
NO_MOCK_REASON
|
||||
)
|
||||
|
||||
|
||||
@skipIf(NO_MOCK, NO_MOCK_REASON)
|
||||
class GetDetailsTestCase(TestCase, LoaderModuleMockMixin):
|
||||
'''Tests for salt.modules.esxcluster.get_details'''
|
||||
def setup_loader_modules(self):
|
||||
return {esxcluster: {'__virtual__':
|
||||
MagicMock(return_value='esxcluster'),
|
||||
'__proxy__': {}}}
|
||||
|
||||
def test_get_details(self):
|
||||
mock_get_details = MagicMock()
|
||||
with patch.dict(esxcluster.__proxy__,
|
||||
{'esxcluster.get_details': mock_get_details}):
|
||||
esxcluster.get_details()
|
||||
mock_get_details.assert_called_once_with()
|
@ -620,6 +620,7 @@ class _GetProxyConnectionDetailsTestCase(TestCase, LoaderModuleMockMixin):
|
||||
'principal': 'fake_principal',
|
||||
'domain': 'fake_domain'}
|
||||
self.esxdatacenter_details = {'vcenter': 'fake_vcenter',
|
||||
'datacenter': 'fake_dc',
|
||||
'username': 'fake_username',
|
||||
'password': 'fake_password',
|
||||
'protocol': 'fake_protocol',
|
||||
@ -627,9 +628,20 @@ class _GetProxyConnectionDetailsTestCase(TestCase, LoaderModuleMockMixin):
|
||||
'mechanism': 'fake_mechanism',
|
||||
'principal': 'fake_principal',
|
||||
'domain': 'fake_domain'}
|
||||
self.esxcluster_details = {'vcenter': 'fake_vcenter',
|
||||
'datacenter': 'fake_dc',
|
||||
'cluster': 'fake_cluster',
|
||||
'username': 'fake_username',
|
||||
'password': 'fake_password',
|
||||
'protocol': 'fake_protocol',
|
||||
'port': 'fake_port',
|
||||
'mechanism': 'fake_mechanism',
|
||||
'principal': 'fake_principal',
|
||||
'domain': 'fake_domain'}
|
||||
|
||||
def tearDown(self):
|
||||
for attrname in ('esxi_host_details', 'esxi_vcenter_details'):
|
||||
for attrname in ('esxi_host_details', 'esxi_vcenter_details',
|
||||
'esxdatacenter_details', 'esxcluster_details'):
|
||||
try:
|
||||
delattr(self, attrname)
|
||||
except AttributeError:
|
||||
@ -651,8 +663,22 @@ class _GetProxyConnectionDetailsTestCase(TestCase, LoaderModuleMockMixin):
|
||||
MagicMock(return_value='esxdatacenter')):
|
||||
with patch.dict(vsphere.__salt__,
|
||||
{'esxdatacenter.get_details': MagicMock(
|
||||
return_value=self.esxdatacenter_details)}):
|
||||
return_value=self.esxdatacenter_details)}):
|
||||
ret = vsphere._get_proxy_connection_details()
|
||||
self.assertEqual(('fake_vcenter', 'fake_username', 'fake_password',
|
||||
'fake_protocol', 'fake_port', 'fake_mechanism',
|
||||
'fake_principal', 'fake_domain'), ret)
|
||||
|
||||
def test_esxcluster_proxy_details(self):
|
||||
with patch('salt.modules.vsphere.get_proxy_type',
|
||||
MagicMock(return_value='esxcluster')):
|
||||
with patch.dict(vsphere.__salt__,
|
||||
{'esxcluster.get_details': MagicMock(
|
||||
return_value=self.esxcluster_details)}):
|
||||
ret = vsphere._get_proxy_connection_details()
|
||||
self.assertEqual(('fake_vcenter', 'fake_username', 'fake_password',
|
||||
'fake_protocol', 'fake_port', 'fake_mechanism',
|
||||
'fake_principal', 'fake_domain'), ret)
|
||||
|
||||
def test_esxi_proxy_vcenter_details(self):
|
||||
with patch('salt.modules.vsphere.get_proxy_type',
|
||||
@ -862,8 +888,8 @@ class GetServiceInstanceViaProxyTestCase(TestCase, LoaderModuleMockMixin):
|
||||
}
|
||||
}
|
||||
|
||||
def test_supported_proxes(self):
|
||||
supported_proxies = ['esxi', 'esxdatacenter']
|
||||
def test_supported_proxies(self):
|
||||
supported_proxies = ['esxi', 'esxcluster', 'esxdatacenter']
|
||||
for proxy_type in supported_proxies:
|
||||
with patch('salt.modules.vsphere.get_proxy_type',
|
||||
MagicMock(return_value=proxy_type)):
|
||||
@ -905,8 +931,8 @@ class DisconnectTestCase(TestCase, LoaderModuleMockMixin):
|
||||
}
|
||||
}
|
||||
|
||||
def test_supported_proxes(self):
|
||||
supported_proxies = ['esxi', 'esxdatacenter']
|
||||
def test_supported_proxies(self):
|
||||
supported_proxies = ['esxi', 'esxcluster', 'esxdatacenter']
|
||||
for proxy_type in supported_proxies:
|
||||
with patch('salt.modules.vsphere.get_proxy_type',
|
||||
MagicMock(return_value=proxy_type)):
|
||||
@ -946,8 +972,8 @@ class TestVcenterConnectionTestCase(TestCase, LoaderModuleMockMixin):
|
||||
}
|
||||
}
|
||||
|
||||
def test_supported_proxes(self):
|
||||
supported_proxies = ['esxi', 'esxdatacenter']
|
||||
def test_supported_proxies(self):
|
||||
supported_proxies = ['esxi', 'esxcluster', 'esxdatacenter']
|
||||
for proxy_type in supported_proxies:
|
||||
with patch('salt.modules.vsphere.get_proxy_type',
|
||||
MagicMock(return_value=proxy_type)):
|
||||
@ -1022,7 +1048,7 @@ class ListDatacentersViaProxyTestCase(TestCase, LoaderModuleMockMixin):
|
||||
}
|
||||
|
||||
def test_supported_proxies(self):
|
||||
supported_proxies = ['esxdatacenter']
|
||||
supported_proxies = ['esxcluster', 'esxdatacenter']
|
||||
for proxy_type in supported_proxies:
|
||||
with patch('salt.modules.vsphere.get_proxy_type',
|
||||
MagicMock(return_value=proxy_type)):
|
||||
@ -1099,7 +1125,7 @@ class CreateDatacenterTestCase(TestCase, LoaderModuleMockMixin):
|
||||
}
|
||||
}
|
||||
|
||||
def test_supported_proxes(self):
|
||||
def test_supported_proxies(self):
|
||||
supported_proxies = ['esxdatacenter']
|
||||
for proxy_type in supported_proxies:
|
||||
with patch('salt.modules.vsphere.get_proxy_type',
|
||||
|
185
tests/unit/proxy/test_esxcluster.py
Normal file
185
tests/unit/proxy/test_esxcluster.py
Normal file
@ -0,0 +1,185 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
:codeauthor: :email:`Alexandru Bleotu <alexandru.bleotu@morganstanley.com>`
|
||||
|
||||
Tests for esxcluster proxy
|
||||
'''
|
||||
|
||||
# Import Python Libs
|
||||
from __future__ import absolute_import
|
||||
|
||||
# Import external libs
|
||||
try:
|
||||
import jsonschema
|
||||
HAS_JSONSCHEMA = True
|
||||
except ImportError:
|
||||
HAS_JSONSCHEMA = False
|
||||
|
||||
# Import Salt Libs
|
||||
import salt.proxy.esxcluster as esxcluster
|
||||
import salt.exceptions
|
||||
from salt.config.schemas.esxcluster import EsxclusterProxySchema
|
||||
|
||||
# Import Salt Testing Libs
|
||||
from tests.support.mixins import LoaderModuleMockMixin
|
||||
from tests.support.unit import TestCase, skipIf
|
||||
from tests.support.mock import (
|
||||
MagicMock,
|
||||
patch,
|
||||
NO_MOCK,
|
||||
NO_MOCK_REASON
|
||||
)
|
||||
|
||||
|
||||
@skipIf(NO_MOCK, NO_MOCK_REASON)
|
||||
@skipIf(not HAS_JSONSCHEMA, 'jsonschema is required')
|
||||
class InitTestCase(TestCase, LoaderModuleMockMixin):
|
||||
'''Tests for salt.proxy.esxcluster.init'''
|
||||
def setup_loader_modules(self):
|
||||
return {esxcluster: {'__virtual__':
|
||||
MagicMock(return_value='esxcluster'),
|
||||
'DETAILS': {}, '__pillar__': {}}}
|
||||
|
||||
def setUp(self):
|
||||
self.opts_userpass = {'proxy': {'proxytype': 'esxcluster',
|
||||
'vcenter': 'fake_vcenter',
|
||||
'datacenter': 'fake_dc',
|
||||
'cluster': 'fake_cluster',
|
||||
'mechanism': 'userpass',
|
||||
'username': 'fake_username',
|
||||
'passwords': ['fake_password'],
|
||||
'protocol': 'fake_protocol',
|
||||
'port': 100}}
|
||||
self.opts_sspi = {'proxy': {'proxytype': 'esxcluster',
|
||||
'vcenter': 'fake_vcenter',
|
||||
'datacenter': 'fake_dc',
|
||||
'cluster': 'fake_cluster',
|
||||
'mechanism': 'sspi',
|
||||
'domain': 'fake_domain',
|
||||
'principal': 'fake_principal',
|
||||
'protocol': 'fake_protocol',
|
||||
'port': 100}}
|
||||
patches = (('salt.proxy.esxcluster.merge',
|
||||
MagicMock(return_value=self.opts_sspi['proxy'])),)
|
||||
for mod, mock in patches:
|
||||
patcher = patch(mod, mock)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def test_merge(self):
|
||||
mock_pillar_proxy = MagicMock()
|
||||
mock_opts_proxy = MagicMock()
|
||||
mock_merge = MagicMock(return_value=self.opts_sspi['proxy'])
|
||||
with patch.dict(esxcluster.__pillar__,
|
||||
{'proxy': mock_pillar_proxy}):
|
||||
with patch('salt.proxy.esxcluster.merge', mock_merge):
|
||||
esxcluster.init(opts={'proxy': mock_opts_proxy})
|
||||
mock_merge.assert_called_once_with(mock_opts_proxy, mock_pillar_proxy)
|
||||
|
||||
def test_esxcluster_schema(self):
|
||||
mock_json_validate = MagicMock()
|
||||
serialized_schema = EsxclusterProxySchema().serialize()
|
||||
with patch('salt.proxy.esxcluster.jsonschema.validate',
|
||||
mock_json_validate):
|
||||
esxcluster.init(self.opts_sspi)
|
||||
mock_json_validate.assert_called_once_with(
|
||||
self.opts_sspi['proxy'], serialized_schema)
|
||||
|
||||
def test_invalid_proxy_input_error(self):
|
||||
with patch('salt.proxy.esxcluster.jsonschema.validate',
|
||||
MagicMock(side_effect=jsonschema.exceptions.ValidationError(
|
||||
'Validation Error'))):
|
||||
with self.assertRaises(salt.exceptions.InvalidConfigError) as \
|
||||
excinfo:
|
||||
esxcluster.init(self.opts_userpass)
|
||||
self.assertEqual(excinfo.exception.strerror.message,
|
||||
'Validation Error')
|
||||
|
||||
def test_no_username(self):
|
||||
opts = self.opts_userpass.copy()
|
||||
del opts['proxy']['username']
|
||||
with patch('salt.proxy.esxcluster.merge',
|
||||
MagicMock(return_value=opts['proxy'])):
|
||||
with self.assertRaises(salt.exceptions.InvalidConfigError) as \
|
||||
excinfo:
|
||||
esxcluster.init(opts)
|
||||
self.assertEqual(excinfo.exception.strerror,
|
||||
'Mechanism is set to \'userpass\', but no '
|
||||
'\'username\' key found in proxy config.')
|
||||
|
||||
def test_no_passwords(self):
|
||||
opts = self.opts_userpass.copy()
|
||||
del opts['proxy']['passwords']
|
||||
with patch('salt.proxy.esxcluster.merge',
|
||||
MagicMock(return_value=opts['proxy'])):
|
||||
with self.assertRaises(salt.exceptions.InvalidConfigError) as \
|
||||
excinfo:
|
||||
esxcluster.init(opts)
|
||||
self.assertEqual(excinfo.exception.strerror,
|
||||
'Mechanism is set to \'userpass\', but no '
|
||||
'\'passwords\' key found in proxy config.')
|
||||
|
||||
def test_no_domain(self):
|
||||
opts = self.opts_sspi.copy()
|
||||
del opts['proxy']['domain']
|
||||
with patch('salt.proxy.esxcluster.merge',
|
||||
MagicMock(return_value=opts['proxy'])):
|
||||
with self.assertRaises(salt.exceptions.InvalidConfigError) as \
|
||||
excinfo:
|
||||
esxcluster.init(opts)
|
||||
self.assertEqual(excinfo.exception.strerror,
|
||||
'Mechanism is set to \'sspi\', but no '
|
||||
'\'domain\' key found in proxy config.')
|
||||
|
||||
def test_no_principal(self):
|
||||
opts = self.opts_sspi.copy()
|
||||
del opts['proxy']['principal']
|
||||
with patch('salt.proxy.esxcluster.merge',
|
||||
MagicMock(return_value=opts['proxy'])):
|
||||
with self.assertRaises(salt.exceptions.InvalidConfigError) as \
|
||||
excinfo:
|
||||
esxcluster.init(opts)
|
||||
self.assertEqual(excinfo.exception.strerror,
|
||||
'Mechanism is set to \'sspi\', but no '
|
||||
'\'principal\' key found in proxy config.')
|
||||
|
||||
def test_find_credentials(self):
|
||||
mock_find_credentials = MagicMock(return_value=('fake_username',
|
||||
'fake_password'))
|
||||
with patch('salt.proxy.esxcluster.merge',
|
||||
MagicMock(return_value=self.opts_userpass['proxy'])):
|
||||
with patch('salt.proxy.esxcluster.find_credentials',
|
||||
mock_find_credentials):
|
||||
esxcluster.init(self.opts_userpass)
|
||||
mock_find_credentials.assert_called_once_with()
|
||||
|
||||
def test_details_userpass(self):
|
||||
mock_find_credentials = MagicMock(return_value=('fake_username',
|
||||
'fake_password'))
|
||||
with patch('salt.proxy.esxcluster.merge',
|
||||
MagicMock(return_value=self.opts_userpass['proxy'])):
|
||||
with patch('salt.proxy.esxcluster.find_credentials',
|
||||
mock_find_credentials):
|
||||
esxcluster.init(self.opts_userpass)
|
||||
self.assertDictEqual(esxcluster.DETAILS,
|
||||
{'vcenter': 'fake_vcenter',
|
||||
'datacenter': 'fake_dc',
|
||||
'cluster': 'fake_cluster',
|
||||
'mechanism': 'userpass',
|
||||
'username': 'fake_username',
|
||||
'password': 'fake_password',
|
||||
'passwords': ['fake_password'],
|
||||
'protocol': 'fake_protocol',
|
||||
'port': 100})
|
||||
|
||||
def test_details_sspi(self):
|
||||
esxcluster.init(self.opts_sspi)
|
||||
self.assertDictEqual(esxcluster.DETAILS,
|
||||
{'vcenter': 'fake_vcenter',
|
||||
'datacenter': 'fake_dc',
|
||||
'cluster': 'fake_cluster',
|
||||
'mechanism': 'sspi',
|
||||
'domain': 'fake_domain',
|
||||
'principal': 'fake_principal',
|
||||
'protocol': 'fake_protocol',
|
||||
'port': 100})
|
268
tests/unit/utils/test_configparser.py
Normal file
268
tests/unit/utils/test_configparser.py
Normal file
@ -0,0 +1,268 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
tests.unit.utils.test_configparser
|
||||
==================================
|
||||
|
||||
Test the funcs in the custom parsers in salt.utils.configparser
|
||||
'''
|
||||
# Import Python Libs
|
||||
from __future__ import absolute_import
|
||||
import copy
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Import Salt Testing Libs
|
||||
from tests.support.unit import TestCase
|
||||
from tests.support.paths import TMP
|
||||
|
||||
# Import salt libs
|
||||
import salt.utils.files
|
||||
import salt.utils.stringutils
|
||||
import salt.utils.configparser
|
||||
|
||||
# The user.name param here is intentionally indented with spaces instead of a
|
||||
# tab to test that we properly load a file with mixed indentation.
|
||||
ORIG_CONFIG = u'''[user]
|
||||
name = Артём Анисимов
|
||||
\temail = foo@bar.com
|
||||
[remote "origin"]
|
||||
\turl = https://github.com/terminalmage/salt.git
|
||||
\tfetch = +refs/heads/*:refs/remotes/origin/*
|
||||
\tpushurl = git@github.com:terminalmage/salt.git
|
||||
[color "diff"]
|
||||
\told = 196
|
||||
\tnew = 39
|
||||
[core]
|
||||
\tpager = less -R
|
||||
\trepositoryformatversion = 0
|
||||
\tfilemode = true
|
||||
\tbare = false
|
||||
\tlogallrefupdates = true
|
||||
[alias]
|
||||
\tmodified = ! git status --porcelain | awk 'match($1, "M"){print $2}'
|
||||
\tgraph = log --all --decorate --oneline --graph
|
||||
\thist = log --pretty=format:\\"%h %ad | %s%d [%an]\\" --graph --date=short
|
||||
[http]
|
||||
\tsslverify = false'''.split(u'\n') # future lint: disable=non-unicode-string
|
||||
|
||||
|
||||
class TestGitConfigParser(TestCase):
|
||||
'''
|
||||
Tests for salt.utils.configparser.GitConfigParser
|
||||
'''
|
||||
maxDiff = None
|
||||
orig_config = os.path.join(TMP, u'test_gitconfig.orig')
|
||||
new_config = os.path.join(TMP, u'test_gitconfig.new')
|
||||
remote = u'remote "origin"'
|
||||
|
||||
def tearDown(self):
|
||||
del self.conf
|
||||
try:
|
||||
os.remove(self.new_config)
|
||||
except OSError as exc:
|
||||
if exc.errno != errno.ENOENT:
|
||||
raise
|
||||
|
||||
def setUp(self):
|
||||
if not os.path.exists(self.orig_config):
|
||||
with salt.utils.files.fopen(self.orig_config, u'wb') as fp_:
|
||||
fp_.write(
|
||||
salt.utils.stringutils.to_bytes(
|
||||
u'\n'.join(ORIG_CONFIG)
|
||||
)
|
||||
)
|
||||
self.conf = salt.utils.configparser.GitConfigParser()
|
||||
self.conf.read(self.orig_config)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
try:
|
||||
os.remove(cls.orig_config)
|
||||
except OSError as exc:
|
||||
if exc.errno != errno.ENOENT:
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def fix_indent(lines):
|
||||
'''
|
||||
Fixes the space-indented 'user' line, because when we write the config
|
||||
object to a file space indentation will be replaced by tab indentation.
|
||||
'''
|
||||
ret = copy.copy(lines)
|
||||
for i, _ in enumerate(ret):
|
||||
if ret[i].startswith(salt.utils.configparser.GitConfigParser.SPACEINDENT):
|
||||
ret[i] = ret[i].replace(salt.utils.configparser.GitConfigParser.SPACEINDENT, u'\t')
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def get_lines(path):
|
||||
with salt.utils.files.fopen(path, u'r') as fp_:
|
||||
return salt.utils.stringutils.to_unicode(fp_.read()).splitlines()
|
||||
|
||||
def _test_write(self, mode):
|
||||
with salt.utils.files.fopen(self.new_config, mode) as fp_:
|
||||
self.conf.write(fp_)
|
||||
self.assertEqual(
|
||||
self.get_lines(self.new_config),
|
||||
self.fix_indent(ORIG_CONFIG)
|
||||
)
|
||||
|
||||
def test_get(self):
|
||||
'''
|
||||
Test getting an option's value
|
||||
'''
|
||||
# Numeric values should be loaded as strings
|
||||
self.assertEqual(self.conf.get(u'color "diff"', u'old'), u'196')
|
||||
# Complex strings should be loaded with their literal quotes and
|
||||
# slashes intact
|
||||
self.assertEqual(
|
||||
self.conf.get(u'alias', u'modified'),
|
||||
u"""! git status --porcelain | awk 'match($1, "M"){print $2}'"""
|
||||
)
|
||||
# future lint: disable=non-unicode-string
|
||||
self.assertEqual(
|
||||
self.conf.get(u'alias', u'hist'),
|
||||
salt.utils.stringutils.to_unicode(
|
||||
r"""log --pretty=format:\"%h %ad | %s%d [%an]\" --graph --date=short"""
|
||||
)
|
||||
)
|
||||
# future lint: enable=non-unicode-string
|
||||
|
||||
def test_read_space_indent(self):
|
||||
'''
|
||||
Test that user.name was successfully loaded despite being indented
|
||||
using spaces instead of a tab. Additionally, this tests that the value
|
||||
was loaded as a unicode type on PY2.
|
||||
'''
|
||||
self.assertEqual(self.conf.get(u'user', u'name'), u'Артём Анисимов')
|
||||
|
||||
def test_set_new_option(self):
|
||||
'''
|
||||
Test setting a new option in an existing section
|
||||
'''
|
||||
self.conf.set(u'http', u'useragent', u'myawesomeagent')
|
||||
self.assertEqual(self.conf.get(u'http', u'useragent'), u'myawesomeagent')
|
||||
|
||||
def test_add_section(self):
|
||||
'''
|
||||
Test adding a section and adding an item to that section
|
||||
'''
|
||||
self.conf.add_section(u'foo')
|
||||
self.conf.set(u'foo', u'bar', u'baz')
|
||||
self.assertEqual(self.conf.get(u'foo', u'bar'), u'baz')
|
||||
|
||||
def test_replace_option(self):
|
||||
'''
|
||||
Test replacing an existing option
|
||||
'''
|
||||
# We're also testing the normalization of key names, here. Setting
|
||||
# "sslVerify" should actually set an "sslverify" option.
|
||||
self.conf.set(u'http', u'sslVerify', u'true')
|
||||
self.assertEqual(self.conf.get(u'http', u'sslverify'), u'true')
|
||||
|
||||
def test_set_multivar(self):
|
||||
'''
|
||||
Test setting a multivar and then writing the resulting file
|
||||
'''
|
||||
orig_refspec = u'+refs/heads/*:refs/remotes/origin/*'
|
||||
new_refspec = u'+refs/tags/*:refs/tags/*'
|
||||
# Make sure that the original value is a string
|
||||
self.assertEqual(
|
||||
self.conf.get(self.remote, u'fetch'),
|
||||
orig_refspec
|
||||
)
|
||||
# Add another refspec
|
||||
self.conf.set_multivar(self.remote, u'fetch', new_refspec)
|
||||
# The value should now be a list
|
||||
self.assertEqual(
|
||||
self.conf.get(self.remote, u'fetch'),
|
||||
[orig_refspec, new_refspec]
|
||||
)
|
||||
# Write the config object to a file
|
||||
with salt.utils.files.fopen(self.new_config, u'w') as fp_:
|
||||
self.conf.write(fp_)
|
||||
# Confirm that the new file was written correctly
|
||||
expected = self.fix_indent(ORIG_CONFIG)
|
||||
expected.insert(6, u'\tfetch = %s' % new_refspec) # pylint: disable=string-substitution-usage-error
|
||||
self.assertEqual(self.get_lines(self.new_config), expected)
|
||||
|
||||
def test_remove_option(self):
|
||||
'''
|
||||
test removing an option, including all items from a multivar
|
||||
'''
|
||||
for item in (u'fetch', u'pushurl'):
|
||||
self.conf.remove_option(self.remote, item)
|
||||
# To confirm that the option is now gone, a get should raise an
|
||||
# NoOptionError exception.
|
||||
self.assertRaises(
|
||||
salt.utils.configparser.NoOptionError,
|
||||
self.conf.get,
|
||||
self.remote,
|
||||
item)
|
||||
|
||||
def test_remove_option_regexp(self):
|
||||
'''
|
||||
test removing an option, including all items from a multivar
|
||||
'''
|
||||
orig_refspec = u'+refs/heads/*:refs/remotes/origin/*'
|
||||
new_refspec_1 = u'+refs/tags/*:refs/tags/*'
|
||||
new_refspec_2 = u'+refs/foo/*:refs/foo/*'
|
||||
# First, add both refspecs
|
||||
self.conf.set_multivar(self.remote, u'fetch', new_refspec_1)
|
||||
self.conf.set_multivar(self.remote, u'fetch', new_refspec_2)
|
||||
# Make sure that all three values are there
|
||||
self.assertEqual(
|
||||
self.conf.get(self.remote, u'fetch'),
|
||||
[orig_refspec, new_refspec_1, new_refspec_2]
|
||||
)
|
||||
# If the regex doesn't match, no items should be removed
|
||||
self.assertFalse(
|
||||
self.conf.remove_option_regexp(
|
||||
self.remote,
|
||||
u'fetch',
|
||||
salt.utils.stringutils.to_unicode(r'\d{7,10}') # future lint: disable=non-unicode-string
|
||||
)
|
||||
)
|
||||
# Make sure that all three values are still there (since none should
|
||||
# have been removed)
|
||||
self.assertEqual(
|
||||
self.conf.get(self.remote, u'fetch'),
|
||||
[orig_refspec, new_refspec_1, new_refspec_2]
|
||||
)
|
||||
# Remove one of the values
|
||||
self.assertTrue(
|
||||
self.conf.remove_option_regexp(self.remote, u'fetch', u'tags'))
|
||||
# Confirm that the value is gone
|
||||
self.assertEqual(
|
||||
self.conf.get(self.remote, u'fetch'),
|
||||
[orig_refspec, new_refspec_2]
|
||||
)
|
||||
# Remove the other one we added earlier
|
||||
self.assertTrue(
|
||||
self.conf.remove_option_regexp(self.remote, u'fetch', u'foo'))
|
||||
# Since the option now only has one value, it should be a string
|
||||
self.assertEqual(self.conf.get(self.remote, u'fetch'), orig_refspec)
|
||||
# Remove the last remaining option
|
||||
self.assertTrue(
|
||||
self.conf.remove_option_regexp(self.remote, u'fetch', u'heads'))
|
||||
# Trying to do a get now should raise an exception
|
||||
self.assertRaises(
|
||||
salt.utils.configparser.NoOptionError,
|
||||
self.conf.get,
|
||||
self.remote,
|
||||
u'fetch')
|
||||
|
||||
def test_write(self):
|
||||
'''
|
||||
Test writing using non-binary filehandle
|
||||
'''
|
||||
self._test_write(mode=u'w')
|
||||
|
||||
def test_write_binary(self):
|
||||
'''
|
||||
Test writing using binary filehandle
|
||||
'''
|
||||
self._test_write(mode=u'wb')
|
Loading…
Reference in New Issue
Block a user