Configuring iDRAC using RedFish API via Ansible.

Having a large number of iDRAC out of band (OOB) interfaces to configure on Dell hardware can seem like a mind numbing daunting task to administrators everywhere. Recently my team was tasked with the following basic tasks on around 80 iDRACs:

  • Hostname
  • DNS Setting
  • Syslog Settings
  • LDAP configuration & RBAC mapping
  • SSL Certificate update (interaction with a Microsoft PKI server (certsrv)
  • NTP configuration
  • Update root credential and place into Hashicorp Vault

After a little research and fiddling with APIs / RACADM and the GUI I settled upon using an Ansible module ‘idrac_redfish_config

Without question its the easiest way to configure the bulk of components within iDRAC. ** Certain elements are not possible with this module – I will cover this later **

To explain how to actually use the module, take this task as an example:

- name: Configure NTP and Timezone on iDRAC
idrac_redfish_config:
category: Manager
command: SetManagerAttributes
manager_attributes:
NTPConfigGroup.1.NTPEnable: "Enabled"
NTPConfigGroup.1.NTP1: "{{ ntp_server }}"
Time.1.Timezone: "{{ timezone }}"
baseuri: "{{ idrac_ip }}"
username: "{{ default_user }}"
password: "{{ idrac_pass }}" 

The manager_attributes section is the key to this module, and typically it is poorly described within the Ansible documentation. The best way to obtain these attributes is to open an iDRAC GUI up, hit F12 to developer mode and track the network activity when saving a change. An attributes POST action includes the payload in the correct format for inclusion in this Ansible task.

My basic playbook for configuration is as follows (excluding certificate change).

---
- name: Configure NTP and Timezone on iDRAC
idrac_redfish_config:
category: Manager
command: SetManagerAttributes
manager_attributes:
NTPConfigGroup.1.NTPEnable: "Enabled"
NTPConfigGroup.1.NTP1: "{{ ntp_server }}"
Time.1.Timezone: "{{ timezone }}"
baseuri: "{{ idrac_ip }}"
username: "{{ default_user }}"
password: "{{ idrac_pass }}" 
- name: Configure DNS Servers
idrac_redfish_config:
category: Manager
command: SetManagerAttributes
manager_attributes:
IPv4Static.1.DNS1: "{{ dns_server.primary }}"
IPv4Static.1.DNS2: "{{ dns_server.secondary }}"
baseuri: "{{ idrac_ip }}"
username: "{{ default_user }}"
password: "{{ idrac_pass }}" 
- name: Configure iDRAC Name and domain 
idrac_redfish_config:
category: Manager
command: SetManagerAttributes
manager_attributes:
NIC.1.DNSDomainName: "{{ domain_name }}"
NIC.1.DNSRacName: "{{ idrac_hostname }}"
NIC.1.DNSRegister: "Enabled"
baseuri: "{{ idrac_ip }}"
username: "{{ default_user }}"
password: "{{ idrac_pass }}" 
- name: Configure TLS 1.2 
idrac_redfish_config:
category: Manager
command: SetManagerAttributes
manager_attributes:
WebServer.1.TLSProtocol: "TLS 1.2 Only"
NIC.1.DNSRacName: "{{ idrac_hostname }}"
NIC.1.DNSRegister: "Enabled"
baseuri: "{{ idrac_ip }}"
username: "{{ default_user }}"
password: "{{ idrac_pass }}" 
- name: Configure LDAPS
idrac_redfish_config:
category: Manager
command: SetManagerAttributes
manager_attributes:
LDAP.1.Enable: "Enabled"
LDAPRoleGroup.1.DN: "CN=DG_IDRAC_ADMINS,OU=1_IDRAC,OU=blah,DC=blah"
LDAPRoleGroup.1.Privilege: 511
LDAPRoleGroup.2.DN: "CN=DG_IDRAC_READ,OU=1_IDRAC,OU=blah,DC=blah"
LDAPRoleGroup.2.Privilege: 1
LDAP.1.BaseDN: "{{ base_dn }}"
LDAP.1.BindDN: "{{ bind_account_name }},{{ base_dn }}"
LDAP.1.BindPassword: "{{ ldap_bind_pwd }}"
LDAP.1.CertValidationEnable: "Disabled"
LDAP.1.Enable: "Enabled"
LDAP.1.GroupAttribute: ""
LDAP.1.GroupAttributeIsDN: "Enabled"
LDAP.1.Port: 636
LDAP.1.SearchFilter: ""
LDAP.1.Server: "{{ domain_name }}"
LDAP.1.UserAttribute: "sAMAccountName"
baseuri: "{{ idrac_ip }}"
username: "{{ default_user }}"
password: "{{ idrac_pass }}" 
- name: Configure Syslog
idrac_redfish_config:
category: Manager
command: SetManagerAttributes
manager_attributes:
SysLog.1.Port: 514
SysLog.1.Server1: 1.2.3.4
SysLog.1.SysLogEnable: "Enabled"
baseuri: "{{ idrac_ip }}"
username: "{{ default_user }}"
password: "{{ idrac_pass }}" 
- name: Login to Vault (Root NS) as Service Account 
uri:
url: "{{ vault_url }}/v1/auth/ldap/login/{{ vault_svc_acc_username }}"
method: POST
body_format: json
body: {"password":"{{ vault_svc_acc_password }}"}
return_content: yes
follow_redirects: all
status_code: 200
validate_certs: no
register: data
no_log: true
- name: Fetch a new password from Vault
uri:
url: "{{ vault_url }}/v1/gen/password"
method: POST
body_format: json
headers:
X-Vault-Token: "{{ data.json.auth.client_token }}"
body: {"length":"15", "digits":"3", "symbols":"3", "allow_uppercase":"true"}
return_content: yes
status_code: 200
validate_certs: no
register: new_pass
no_log: true
- name: Login to Vault as Service Account
uri:
url: "{{ vault_url }}/v1/auth/ldap/login/{{ vault_svc_acc_username }}"
method: POST
body_format: json
headers:
X-Vault-Namespace: "{{ vault_namespace }}"
body: {"password":"{{ vault_svc_acc_password }}"}
return_content: yes
status_code: 200
validate_certs: no
register: ns_data
no_log: true
- name: Post new server record to Vault - iDRAC
uri:
url: "{{ vault_url }}/v1/data/servers/idrac/{{ idrac_hostname | lower}}/root"
body_format: json
method: POST
headers:
X-Vault-Token: "{{ ns_data.json.auth.client_token }}"
X-Vault-Namespace: "{{ vault_namespace }}"
body: { "options": { "max_versions": "12" }, "data": { "password":"{{ new_pass.json.data.value }}" }}
return_content: yes
status_code: 200
validate_certs: no
no_log: true
- name: Change Root Password
idrac_redfish_config:
category: Manager
command: SetManagerAttributes
manager_attributes:
Users.2.AuthenticationProtocol: "SHA"
Users.2.EmailAddress: ""
Users.2.Enable: "Enabled"
Users.2.IpmiLanPrivilege: "Administrator"
Users.2.IpmiSerialPrivilege: "Administrator"
Users.2.Password: "{{ new_pass.json.data.value }}"
Users.2.PrivacyProtocol: "AES"
Users.2.Privilege: 511
Users.2.ProtocolEnable: "Disabled"
Users.2.Simple2FA: "Disabled"
Users.2.SolEnable: "Enabled"
Users.2.UseEmail: "Disabled"
Users.2.UserName: "root"
baseuri: "{{ idrac_ip }}"
username: "{{ default_user }}"
password: "{{ idrac_pass }}" 

Certificates…

Of course changing the SSL certificate wouldn’t be as easy as all the elements above. The Ansible module does not support the changing of the certificate for some unknown reason, so I had to delve into the hell which is the Dell developer website, specifically for these poorly described API calls….

https://developer.dell.com/apis/2978/versions/5.xx/openapi.yaml/paths/~1redfish~1v1~1CertificateService~1Actions~1CertificateService.GenerateCSR/post

https://developer.dell.com/apis/2978/versions/5.xx/openapi.yaml/paths/~1redfish~1v1~1CertificateService~1Actions~1CertificateService.ReplaceCertificate/post

The flow for this playbook is as follows:

  • Generate CSR on the iDRAC and export
  • Send CSR to Microsoft PKI server and request against a specific template
  • Obtain certificate for certsrv
  • Parse cert and upload to iDRAC
  • Reset iDRAC for cert to take effect
--
- name: Generate iDRAC CSR 
uri:
url: "https://{{ idrac_ip }}/redfish/v1/CertificateService/Actions/CertificateService.GenerateCSR"
method: POST
url_username: "{{ default_user}}"
url_password: "{{ idrac_pass }}"
force_basic_auth: yes
return_content: yes
body_format: json
status_code: 200
validate_certs: no
body: '{
"Country" : "GB",
"State" : "StateHere",
"City"  : "Cityhere",
"Organization" : "Blah",
"OrganizationalUnit" : "BlahOU",
"CommonName" : "{{ idrac_hostname }}.{{ domain_name }}",
"AlternativeNames" : [
"{{ idrac_hostname }}.{{ domain_name }}",
"{{ idrac_hostname }}"
],
"CertificateCollection" : {
"@odata.id" : "/redfish/v1/Managers/iDRAC.Embedded.1/NetworkProtocol/HTTPS/Certificates"
}
}'
no_log: false
register: data
- name: Set CSR as new fact
set_fact:
new_csr: "{{ data.json.CSRString | replace('\n','') }}"
- name: Print out the CSR
debug:
var: new_csr
- name: Send CSR to Microsoft PKI server
uri:
url: "https://{{ pki_server }}/certsrv/certfnsh.asp"
method: POST
url_username: "{{ pki_svc_account }}@{{ domain_name | upper }}"
url_password: "{{ pki_pwd }}"
use_gssapi: yes
return_content: yes
body_format: form-urlencoded
status_code: 200
validate_certs: no
body: 
Mode: "newreq"
CertRequest: "{{ new_csr }}"
CertAttrib: "CertificateTemplate:{{ pki_template }}"
FriendlyType: "Saved-Request Certificate"
TargetStoreFlags: "0"
SaveCert: "yes"
no_log: true
register: cert_out
- name: Set content as a fact
set_fact:
content: "{{ cert_out.content }}"
- name: Push new fact to file
copy:
content: "{{ content }}"
dest: ./content
- name: Grab Request ID 
shell: pwsh -c "./regex_search.ps1"
register: req_id
- name: print req id
debug:
msg: "{{ req_id }}"
- name: Obtain certificate from PKI server
uri:
url: "https://{{ pki_server }}/certsrv/certnew.cer?ReqID={{ req_id.stdout }}&Enc=b64"
method: GET
url_username: "{{ pki_svc_account }}@{{ domain_name | upper }}"
url_password: "{{ pki_pwd }}"
use_gssapi: yes
return_content: yes
status_code: 200
validate_certs: no
no_log: true
register: new_cert
- name: Parse the new certificate to clear up the rubbish \r instances returned from MS
set_fact:
cert: "{{ new_cert.content | regex_replace('\\r?','') }}"
- name: Print the new parsed certificate
debug:
var: cert
- name: Upload Certificate to iDRAC
uri:
url: "https://{{ idrac_ip }}/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/DelliDRACCardService/Actions/DelliDRACCardService.ImportSSLCertificate"
method: POST
url_username: "{{ default_user}}"
url_password: "{{ idrac_pass }}"
force_basic_auth: yes
return_content: yes
body_format: json
status_code: 200
validate_certs: no
body: '{
"SSLCertificateFile" : "{{ cert }}",
"CertificateType" : "Server"
}'
no_log: true
register: data
- name: Reset IDRAC to allow the certificate take effect
uri:
url: "https://{{ idrac_ip }}/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Manager.Reset"
method: POST
url_username: "{{ default_user}}"
url_password: "{{ idrac_pass }}"
force_basic_auth: yes
return_content: yes
body_format: json
status_code: 204
validate_certs: no
body: '{
"ResetType" : "GracefulRestart"
}'
no_log: false
- name: "Wait for {{ idrac_ip }} to come up"
uri:
url: "https://{{ idrac_ip }}//restgui/start.html"
validate_certs: no
method: GET
follow_redirects: none
register: _result
until: _result.status == 200
retries: 30
delay: 20
# 5 minute wait time

For these interested, I have a small PowerShell hack in this code to grab the ReqID from the output from the PKI server which is….

I ran out of time to integrate this into a Ansible regex_search task, but I’m sure its possible!

regex_search.ps1
$reqID = (get-content ./content | select-string -Pattern "ReqID=[0-9]{1,5}" | select-object -Index 0).Matches.Value.Split("=")[1]
write-host $reqID

I hope this helps someone out!!

Leave a Reply